# Corven Email SDK > Send transactional email with Effect. Configure provider Layers, validate messages and handle typed submission outcomes. # Corven Email SDK Source: https://email-sdk.corvencore.com/ Corven Email SDK is a TypeScript library for Effect applications. Use `Email` to validate and send messages through a configured provider account. Provide the account Layer and transport in your application. For offline tests, use an in-memory recording Layer. The SDK prepares messages and applies submission policy. Your application supplies credentials and stores queue and recovery state. The provider processes requests and delivers email. ## Get started 1. [Install the package](/getting-started/installation) for your supported Node and Effect versions. 2. [Record your first email](/quickstart) without credentials or network calls. 3. [Acquire account credentials](/getting-started/credentials) and [configure sending](/sending). 4. Choose a [provider guide](/providers) and validate the complete message. ## Providers and sending modes Each [provider guide](/providers) explains configuration, supported intent, acknowledgments and replay limits. Use [field support](/providers/field-support) to compare local contracts, then run [preflight](/providers/capabilities) for actual provider rules. Use ordinary `Email` for literal messages, batching and individual personalization. Native SendGrid/Mailgun audiences share a provider request. [Loops](/providers/loops) and [Iterable](/providers/iterable) use separate provider-managed job services. See [sending modes](/concepts/sending-modes) before treating these operations as interchangeable. ## Build application workflows - [Batch and route messages](/batching-routing) or [personalize a template](/guides/personalization). - [Apply defaults and transformations](/composition), then review the same prepared operation you dispatch. - [Observe minimal facts](/composition/observations) or enable [authorized private capture](/composition/capture). - [Render React bodies and application templates](/ui). - [Approve agent requests](/integrations/agent), expose an [AI tool](/integrations/ai), or process [webhook reports](/integrations/webhooks). ## Operate and recover A receipt describes the submission result. An accepted receipt does not confirm delivery. An unknown result means that the SDK could not determine whether the provider accepted the request. See [receipts and retries](/reliability) and [idempotency](/concepts/idempotency) before implementing retries. Use the guides for [testing](/guides/testing), [scheduling](/guides/scheduling), [production pipelines](/guides/production-pipeline), [troubleshooting](/guides/troubleshooting) and [migration](/guides/migration). The [API reference](/reference) covers exact contracts and the offline CLI. ## Machine-readable documentation [llms.txt](/llms.txt) is the categorized index. Append `.md` to a guide URL for its Markdown mirror, or use [llms-full.txt](/llms-full.txt) for the complete corpus. The [agent guide](/agents) describes focused retrieval and host-owned execution authority. This documentation site does not authenticate accounts or invoke a model. --- # Documentation for coding agents Source: https://email-sdk.corvencore.com/agents Use [llms.txt](/llms.txt) to find a provider or task page. Append `.md` to the page URL to retrieve Markdown. Use [llms-full.txt](/llms-full.txt) to retrieve all pages. The static [JSON page index](/api/docs/pages.json) links per-page JSON and Markdown forms. [agent-readability.json](/agent-readability.json) describes the emitted retrieval surfaces. These endpoints serve documentation. They do not provide credentials or execute sends. ## Integrate the library 1. Check [installation and runtime](/getting-started/installation) against the consuming application. 2. Import the common service from `@corven/email-sdk` and the account Layer from the selected [provider subpath](/providers). 3. Use fictional inputs and the [recording service](/guides/testing) first. 4. Validate complete intended fields and options before any authorized provider execution. 5. Preserve typed receipts, unknown outcomes and application-owned recovery state. Keep source/declaration checks separate from runtime, authentication and delivery evidence. Do not copy provider flags or CLI commands from a different SDK, even when a service name matches. ## Expose a constrained tool Use [EmailAgentApproval](/integrations/agent) and the scoped [AI tool bridge](/integrations/ai). Models propose limited content; the host owns sender, accounts, credentials, policy, keys and the approval decision. Tool-call IDs and model confirmation do not grant replay permission. This documentation site does not host MCP, generate an installable agent skill, invoke a model or collect provider credentials. The SDK has no automatic account discovery or import-time provider calls. --- # Batching, personalization and routing Source: https://email-sdk.corvencore.com/batching-routing Use `sendAll` for independent messages, `sendPersonalized` for one template and an audience, or named account routing to choose where a message is submitted. ## Send a batch `sendAll` accepts an array of `{ message, options? }` envelopes and a bounded concurrency setting: ```ts import { Effect } from "effect"; import { Email } from "@corven/email-sdk"; export const sendNotifications = Effect.gen(function* sendNotifications() { const email = yield* Email; return yield* email.sendAll( [ { message: { body: { text: "A fictional notification." }, from: "sender@example.test", subject: "Notification", to: "recipient@example.test", }, }, ], { concurrency: 2 }, ); }); ``` Provide an `Email` Layer when running this program, as shown in [sending](/sending) or the offline [quickstart](/quickstart). Each request has its own key and delivery policy. There is no batch-wide key for unrelated messages. Results retain input order even when messages finish in a different order; expected item failures appear as Effect Results. Defects and interruption cancel active work and propagate. Already accepted messages cannot be undone. A batch is neither an atomic transaction nor an implicit provider-native bulk request. ## Personalize a message `sendPersonalized` takes a template without recipient fields and an explicit audience. Each audience member supplies a mailbox, template variables and an optional idempotency key. Shared options specify concurrency and context, not one key for the whole audience. With the default `Individual` mode, the SDK expands the template into per-recipient messages. Use `renderEmailPersonalizedMessage` to preview expansion without a sender. HTML expansion escapes text markup, but does not validate URL or CSS contexts or sanitize arbitrary HTML. Missing variables and invalid expanded messages remain failures. ### Use native personalization Opt-in SendGrid and Mailgun Layers can submit a valid subset of the audience in one native request. Recipients then share request-level acknowledgment, deadline and correlation. Check the configured `personalization` mode. Common defaults, transformation wrappers and ordinary routing reject `NativeAudience` Layers; they do not silently expand them into individual sends. A shared method name does not imply independent wire requests or per-recipient acknowledgment. ## Route explicit accounts Register a Layer for each named account, then select a route through `EmailRouter`: ```ts import { Effect, Layer } from "effect"; import type { Redacted } from "effect"; import { EmailRouter, emailRouterLayer } from "@corven/email-sdk"; import { postmarkEmailLayer } from "@corven/email-sdk/postmark"; import { resendEmailLayer } from "@corven/email-sdk/resend"; import { emailHttpLayer } from "./sending.js"; export const sendRoutedNotification = ( resendKey: Redacted.Redacted, postmarkToken: Redacted.Redacted, ) => { const routes = emailRouterLayer( [ { layer: resendEmailLayer({ apiKey: resendKey }), name: "primary" }, { layer: postmarkEmailLayer({ serverToken: postmarkToken }), name: "backup" }, ], { fallbacks: ["backup"], primary: "primary" }, ).pipe(Layer.provide(emailHttpLayer)); return Effect.gen(function* sendRoutedNotification() { const router = yield* EmailRouter; const email = yield* router.select(); return yield* email.send({ body: { text: "A fictional routed notification." }, from: "sender@example.test", subject: "Notification", to: "recipient@example.test", }); }).pipe(Effect.provide(routes)); }; ``` The example uses the HTTP Layer from [sending](/sending) and deliberately omits an idempotency key: every candidate, including Postmark, must support the requested key. Before transport, the router preflights the effective message, options and policy for every selected candidate. An unsupported field or key on a backup fails locally instead of being dropped. ### Understand when fallback stops Only an eligible definite rejection advances to a fallback account. Unknown outcomes, conflicts, terminal receipts, defects and interruption stop the chain. Partial, suppressed and simulated receipts are terminal outcomes, not reasons to try a backup. Routed receipts identify the account selected. One routing deadline covers the whole chain rather than resetting for each account. Persist route selection for recovery across later executions. If a backup previously accepted the email, restarting at the primary can duplicate it. Native replay guarantees remain local to a provider account; they do not transfer across routes. See [receipts, errors and retries](/reliability). --- # Defaults, preparation and observations Source: https://email-sdk.corvencore.com/composition Add wrappers to a configured account Layer to apply defaults, transform messages or observe results. Wrapper order determines whether callbacks receive values before or after defaults. ## Apply message defaults `defaultedEmailLayer`, from `@corven/email-sdk/defaults`, applies reply-to, header, tag and metadata defaults, plus prefixes for caller-supplied keys. Set the sender, body and event key explicitly. The resulting message still passes provider validation. `transformedEmailLayer`, from the root entrypoint, runs trusted application preparation and can return replacement message content. A transformation cannot replace operation policy or keys, or grant permission to dispatch after a failure. Supply Effects whose dependencies are already resolved. Both wrappers require `Individual` personalization and reject `NativeAudience` Layers with `EmailConfigurationError`. Loops and Iterable have separate defaults entrypoints for their native jobs. ## Prepare a message for review Use `email.prepare(message, options?)` when your application needs to review the effective message before dispatch. It returns: - `previews`: owned candidate messages, options and route paths after preparation. - `validate`: an Effect for local validation. - `send`: an Effect for dispatching the prepared operation. Review these previews, then validate or send that same operation while its supplying Layer scope remains open. Defaults and transformations must not run again after approval and change what was reviewed. Include keys, route paths and private execution context in the review decision. Preparation does not submit a request, reserve remote capacity or extend a scope's lifetime. The operation is an in-process value, not a serialized approval token. ## Choose what to observe | Need | Public API | | --------------------------------------------- | ---------------------------------------------------------------------------- | | Minimal settled sender-call facts | `@corven/email-sdk/observations` | | Bounded private input and outcome capture | `@corven/email-sdk/capture`, with explicit payload-capture permission | | Loops or Iterable operation facts and capture | `@corven/email-sdk/native-observations` / `@corven/email-sdk/native-capture` | | Complete private prepared-operation outcomes | Root `observedEmailOperationLayer` | | Read-only named samples from existing views | Capture and observation aggregation capabilities | Select the API for the operation you need to observe. Sender calls, prepared operations and grouped provider requests are recorded separately. A request can be submitted without appearing in a particular observation view. Reuse the same Layer values for sending and querying so both paths acquire the same stores. Aggregated samples are not a globally atomic history or a lifetime delivery count. ## Protect captured data and outcomes Callbacks are awaited application Effects. Their failures cannot roll back a provider acknowledgment or authorize retry or fallback. Minimal observations exclude private payloads. Payload capture and complete operation callbacks expose private application data, so define access and retention rules before enabling them. Purging a visible capture or recording view does not securely erase previously returned snapshots or reset duplicate-prevention evidence. ## Detailed guides - [Message defaults](/composition/defaults) and [transformations](/composition/transformations). - [Minimal submission observations](/composition/observations), [private capture](/composition/capture) and [complete operation callbacks](/composition/operations). - [Named account aggregation](/composition/aggregation). - [Native job defaults](/composition/native-defaults) and [instrumentation](/composition/native-instrumentation). For approval-driven workflows, see [agent and AI tools](/integrations#host-approved-agent-and-ai-tool). --- # Aggregate account observations and capture Source: https://email-sdk.corvencore.com/composition/aggregation Use named aggregates when your application needs one read surface over several existing account views. They sample configured observation/capture stores; they do not submit messages or create durable history. ## Aggregate minimal observations ```ts import { Effect, Layer } from "effect"; import { EmailRouter, emailLayer, observedEmailRouterLayer } from "@corven/email-sdk"; import { EmailObservationAggregate, observedEmailSenderLayer, } from "@corven/email-sdk/observations"; import { recordingSenderLayer } from "@corven/email-sdk/testing"; const first = emailLayer().pipe( Layer.provideMerge(observedEmailSenderLayer(Layer.fresh(recordingSenderLayer), { capacity: 2 })), ); const second = emailLayer().pipe( Layer.provideMerge(observedEmailSenderLayer(Layer.fresh(recordingSenderLayer), { capacity: 3 })), ); const router = observedEmailRouterLayer( [ { layer: first, name: "first" }, { layer: second, name: "second" }, ], { primary: "first" }, ); export const fictionalObservationAggregation = Effect.gen(function* queryNamedObservations() { const route = yield* (yield* EmailRouter).select({ primary: "second" }); yield* route.send({ body: { text: "A fictional fact aggregation example." }, from: "sender@fictional.invalid", subject: "Fictional notification", to: "recipient@fictional.invalid", }); return yield* (yield* EmailObservationAggregate).snapshot; }).pipe(Effect.provide(router)); ``` `observedEmailRouterLayer` keeps named account views available through `EmailObservationAggregate`. Each source identifies its unit: `SenderInvocation` or `NativeOperation`. Retained counts and truncation describe the samples, not lifetime sends or deliveries. ## Aggregate private capture ```ts import { Effect, Layer } from "effect"; import { Email, emailLayer, capturedEmailRouterLayer } from "@corven/email-sdk"; import { EmailCaptureAggregate, capturedEmailSenderLayer } from "@corven/email-sdk/capture"; import { recordingSenderLayer } from "@corven/email-sdk/testing"; const options = { capacity: 100, maxBytes: 1_048_576, privateData: "AuthorizePayloadCapture", } as const; const primary = emailLayer().pipe( Layer.provideMerge(capturedEmailSenderLayer(Layer.fresh(recordingSenderLayer), options)), ); const backup = emailLayer().pipe( Layer.provideMerge(capturedEmailSenderLayer(Layer.fresh(recordingSenderLayer), options)), ); const router = capturedEmailRouterLayer( [ { layer: primary, name: "primary" }, { layer: backup, name: "backup" }, ], { fallbacks: ["backup"], primary: "primary" }, ); export const fictionalAggregateCapture = Effect.gen(function* inspectFictionalAccounts() { yield* (yield* Email).send({ body: { text: "A fictional recording example." }, from: "sender@fictional.invalid", subject: "Fictional aggregate capture", to: "recipient@fictional.invalid", }); return yield* (yield* EmailCaptureAggregate).snapshot; }).pipe(Effect.provide(router)); ``` `capturedEmailRouterLayer` supplies `EmailCaptureAggregate` over authorized capture views. Read `retainedRecords`, `retainedBytes`, source snapshots and `truncated`; keep payloads private. ## Keep acquisition and failure semantics Reuse the same Layer values used by sending so queries observe the same acquired stores. Source names must be distinct bounded identifiers, not private account values. Aggregation returns its exported aggregate error if a sample cannot be read or validated. Samples across sources are not globally atomic. Missing data, eviction or a closed view does not prove non-submission and must not reset route/key recovery evidence. --- # Private submission capture Source: https://email-sdk.corvencore.com/composition/capture Use `capturedEmailSenderLayer` from `@corven/email-sdk/capture` only when your application is authorized to retain message payloads: ```ts import { Effect, Layer } from "effect"; import { Email, emailLayer } from "@corven/email-sdk"; import type { EmailMessageInput } from "@corven/email-sdk"; import { capturedEmailSenderLayer, EmailSubmissionCapture } from "@corven/email-sdk/capture"; import { recordingSenderLayer } from "@corven/email-sdk/testing"; export const captureSyntheticSubmission = Effect.fnUntraced(function* captureSyntheticSubmission( message: EmailMessageInput, ) { const email = yield* Email; const capture = yield* EmailSubmissionCapture; const receipt = yield* email.send(message); const snapshot = yield* capture.snapshot; const purgedRecordCount = yield* capture.purgeRecords; return { purgedRecordCount, receipt, snapshot }; }); export const capturedSyntheticEmailLayer = emailLayer().pipe( Layer.provideMerge( capturedEmailSenderLayer(recordingSenderLayer, { capacity: 10, maxBytes: 1_048_576, privateData: "AuthorizePayloadCapture", }), ), ); ``` The example uses a recorder, so it does not contact a provider. In production, substitute the configured sender without changing the capture/query ownership. ## Configure retention Supply `capacity`, `maxBytes` and `privateData: "AuthorizePayloadCapture"`. Retention is bounded by record count and UTF-8 bytes. Oversized or evicted data is reflected in `truncated`; the view is not a complete audit log. `EmailSubmissionCapture.snapshot` returns `records`, `retainedBytes`, `closed` and `truncated`. Each record's `payloadJson` is Redacted. Unwrap and decode it only in an explicitly authorized private path using the exported payload Schema; do not log it as a diagnostic shortcut. ## Purge the visible view `purgeRecords` removes visible records and returns the count removed. It does not securely erase copies already returned to callers, clear every private application store or reset duplicate-prevention evidence. Keep sending and querying in the same Layer acquisition. Define access control and retention outside the SDK before replacing the synthetic sender with a real account. Use [minimal observations](/composition/observations) when payloads are unnecessary. --- # Message defaults Source: https://email-sdk.corvencore.com/composition/defaults Wrap an account with `defaultedEmailLayer` from `@corven/email-sdk/defaults`: ```ts import { Effect, Redacted } from "effect"; import { Email } from "@corven/email-sdk"; import { defaultedEmailLayer } from "@corven/email-sdk/defaults"; import { resendEmailLayer } from "@corven/email-sdk/resend"; const account = resendEmailLayer({ apiKey: Redacted.make("fictional-unused-key") }); const defaultedAccount = defaultedEmailLayer(account, { headers: [{ name: "X-Application", value: "fictional-example" }], idempotencyKeyPrefix: "fictional-application/", replyTo: { address: "replies@fictional.invalid", name: "Fictional application" }, tags: [{ name: "application", value: "fictional-example" }], }); export const fictionalDefaultedSubmission = Effect.gen(function* sendDefaultedNotification() { const email = yield* Email; return yield* email.send( { body: { text: "A fictional defaults example." }, from: "sender@fictional.invalid", subject: "Fictional notification", to: "recipient@fictional.invalid", }, { idempotencyKey: "caller-owned-logical-event" }, ); }).pipe(Effect.provide(defaultedAccount)); ``` The example leaves `HttpClient` as a required service. Provide your [safe transport Layer](/sending) in the application entrypoint. ## Merge rules A caller's reply-to takes precedence over the default. Default headers and tags precede caller entries, so duplicates can still fail provider validation. Metadata merges with caller values taking precedence. `idempotencyKeyPrefix` prefixes a caller-supplied key; it does not generate a key or authorize unrelated events to share one. The effective key must still satisfy the selected account's length and alphabet restrictions. Sender, subject and body are not defaults in this wrapper. Set them explicitly in your notification program or a trusted transformation. ## Preparation order Defaults are part of effective intent. Review the prepared message after they have been applied, and dispatch that same operation after approval. Wrapper order affects which values another transform or observer sees. The common wrapper requires `Individual` personalization. A `NativeAudience` Layer fails acquisition with `EmailConfigurationError`. Use the separate [native defaults](/composition/native-defaults) for Loops or Iterable jobs. --- # Native job defaults Source: https://email-sdk.corvencore.com/composition/native-defaults Loops and Iterable use native jobs rather than the common literal-message envelope. Their defaults live in separate entrypoints. ## Loops ```ts import type { Layer } from "effect"; import { Effect } from "effect"; import { defaultedLoopsTransactionalLayer } from "@corven/email-sdk/loops-defaults"; import { LoopsTransactionalEmail } from "@corven/email-sdk/loops"; export const fictionalDefaultedLoopsSubmission = ( account: Layer.Layer, ) => Effect.gen(function* sendDefaultedLoopsNotification() { const loops = yield* LoopsTransactionalEmail; return yield* loops.send( { dataVariables: { items: [{ amount: 0, title: "Fictional item" }] }, email: "recipient@fictional.invalid", transactionalId: "host_selected_published_template", }, { idempotencyKey: "caller-owned-logical-event" }, ); }).pipe( Effect.provide( defaultedLoopsTransactionalLayer(account, { dataVariables: { application: "Fictional application" }, idempotencyKeyPrefix: "fictional-application/", }), ), ); ``` `defaultedLoopsTransactionalLayer` supplies default `dataVariables` and an optional caller-key prefix. The application still selects the published template and recipient. It does not transform a native template into common `Email` content. ## Iterable ```ts import { Effect, Redacted } from "effect"; import { defaultedIterableCampaignLayer } from "@corven/email-sdk/iterable-defaults"; import { IterableCampaignEmail, iterableCampaignLayer } from "@corven/email-sdk/iterable"; export const defaultedCampaignLayer = defaultedIterableCampaignLayer( iterableCampaignLayer({ apiKey: Redacted.make("fictional-key"), campaignAuthority: "AuthorizeProviderManagedCampaign", contactWrites: "AuthorizeProfileCreation", dataRetention: "AuthorizePersistedSendData", projectIdentity: "EmailBased", region: "US", tracking: "AuthorizeCampaignTracking", }), { dataFields: { application: "fictional" }, metadata: { workflow: "fictional" } }, ); export const previewDefaultedCampaign = Effect.gen(function* previewDefaultedCampaign() { return yield* (yield* IterableCampaignEmail).validate({ campaignId: 123, dataFields: { application: "operation override", enabled: false }, recipientEmail: "recipient@fictional.invalid", }); }); ``` `defaultedIterableCampaignLayer` merges default data fields and metadata. Caller values take precedence. The resulting job still passes native campaign validation, including project identity and scheduling constraints. The examples leave HTTP transport to the application. Configuration attestations retain their original meaning after wrapping: defaults do not inspect remote templates, enable attachments, disable tracking or acquire profile-write permission. See [Loops](/providers/loops) and [Iterable](/providers/iterable) for the native input and replay boundaries. --- # Native job observations and capture Source: https://email-sdk.corvencore.com/composition/native-instrumentation Use native instrumentation for `LoopsTransactionalEmail` and `IterableCampaignEmail`. Common sender-call wrappers do not describe these specialized service operations. ## Minimal operation facts ```ts import { Effect, Redacted } from "effect"; import { EmailNativeObservations, observedLoopsTransactionalLayer, } from "@corven/email-sdk/native-observations"; import type { EmailNativeObserver, EmailNativeStartingObserver, } from "@corven/email-sdk/native-observations"; import { LoopsTransactionalEmail, loopsTransactionalLayer } from "@corven/email-sdk/loops"; export const observedNativeLoopsLayer = ( observer: EmailNativeObserver, startingObserver?: EmailNativeStartingObserver, ) => observedLoopsTransactionalLayer( loopsTransactionalLayer({ apiKey: Redacted.make("fictional-unused-key"), attachments: "Disabled", templateAuthority: "ProviderManaged", }), { capacity: 100 }, observer, startingObserver, ); export const inspectNativeOperationFacts = Effect.gen(function* inspectNativeOperationFacts() { const loops = yield* LoopsTransactionalEmail; const view = yield* EmailNativeObservations; const preview = yield* loops.validate({ email: "recipient@fictional.invalid", transactionalId: "fictional-template", }); return { observations: yield* view.snapshot, preview }; }); ``` `observedLoopsTransactionalLayer` supplies `EmailNativeObservations`; the Iterable counterpart wraps campaign operations. A snapshot retains bounded `Receipt`, `Rejected` and `Unknown` facts with the native operation kind. The example validates a job; local validation alone is not a submitted-operation observation. ## Authorized private capture ```ts import { Effect, Redacted, Schema } from "effect"; import { EmailNativeCapture, EmailNativeCapturePayloadSchema, capturedLoopsTransactionalLayer, } from "@corven/email-sdk/native-capture"; import { LoopsTransactionalEmail, loopsTransactionalLayer } from "@corven/email-sdk/loops"; export const capturedNativeLoopsLayer = capturedLoopsTransactionalLayer( loopsTransactionalLayer({ apiKey: Redacted.make("fictional-unused-key"), attachments: "Disabled", templateAuthority: "ProviderManaged", }), { capacity: 100, maxBytes: 8_388_608, privateData: "AuthorizePayloadCapture" }, ); export const inspectPrivateNativeCapture = Effect.gen(function* inspectPrivateNativeCapture() { const loops = yield* LoopsTransactionalEmail; yield* loops.validate({ email: "recipient@fictional.invalid", transactionalId: "fictional-template", }); const capture = yield* EmailNativeCapture; const snapshot = yield* capture.snapshot; const payloads = yield* Effect.forEach((payload: string) => Schema.decodeEffect(Schema.fromJsonString(EmailNativeCapturePayloadSchema))(payload), )(snapshot.records.map((record) => Redacted.value(record.payloadJson))); const purgedRecordCount = yield* capture.purgeRecords; return { payloads, purgedRecordCount, snapshot }; }); ``` `capturedLoopsTransactionalLayer` and `capturedIterableCampaignLayer` require count/byte bounds and `AuthorizePayloadCapture`. `EmailNativeCapture` exposes a Redacted payload view plus `purgeRecords`. Decode with `EmailNativeCapturePayloadSchema` only in an authorized private path. Provide the configured HTTP transport before running these programs. Keep views and sending in the same acquisition. Callback failure cannot undo acknowledgment, truncation does not prove non-submission, and purging is not secure erasure of escaped data or permission to repeat a job. --- # Submission observations Source: https://email-sdk.corvencore.com/composition/observations Use `observedEmailSenderLayer` from `@corven/email-sdk/observations` around an `EmailSender`, then build the common `Email` service: ```ts import { Effect, Layer } from "effect"; import { Email, emailLayer } from "@corven/email-sdk"; import type { EmailMessageInput } from "@corven/email-sdk"; import { EmailSubmissionObservations, observedEmailSenderLayer, } from "@corven/email-sdk/observations"; import type { EmailSubmissionObserver, EmailSubmissionStartingObserver, } from "@corven/email-sdk/observations"; import { recordingSenderLayer } from "@corven/email-sdk/testing"; export const submitAndInspectSyntheticEmail = Effect.fnUntraced( function* submitAndInspectSyntheticEmail(message: EmailMessageInput) { const email = yield* Email; const observations = yield* EmailSubmissionObservations; const receipt = yield* email.send(message); const snapshot = yield* observations.snapshot; return { receipt, snapshot }; }, ); export const observedSyntheticEmailLayer = ( observer: EmailSubmissionObserver, startingObserver?: EmailSubmissionStartingObserver, ) => emailLayer().pipe( Layer.provideMerge( observedEmailSenderLayer(recordingSenderLayer, { capacity: 10 }, observer, startingObserver), ), ); ``` `Layer.provideMerge` keeps the observation query service available alongside sending. Acquire both from the same composed Layer. ## Read the view `EmailSubmissionObservations.snapshot` returns retained observations and `truncated`. Settled cases are `Receipt`, `Rejected` and `Unknown`. A receipt observation includes only its kind and provider; it does not retain recipients, message content, keys or original private causes. The optional starting callback describes a sender invocation. It is a separate event from settled history, not evidence that the provider accepted a message. ## Choose the correct unit A sender call is not necessarily a whole logical workflow, an audience or a lifetime delivery count. Local validation can fail before this observation boundary. For complete prepared-operation outcomes, use [operation observations](/composition/operations); for Loops/Iterable use [native instrumentation](/composition/native-instrumentation). Callbacks are awaited Effects. A callback failure can stop the caller's workflow, but cannot roll back a provider acknowledgment or authorize retry/fallback. Treat missing data and truncated history as incomplete evidence. --- # Prepared-operation observations Source: https://email-sdk.corvencore.com/composition/operations Use root `observedEmailOperationLayer` when a sender-call summary is insufficient for application review or auditing: ```ts import { Effect } from "effect"; import { Email, observedEmailOperationLayer } from "@corven/email-sdk"; import type { EmailOperationObserver } from "@corven/email-sdk"; import { defaultedEmailLayer } from "@corven/email-sdk/defaults"; import { recordingEmailLayer } from "@corven/email-sdk/testing"; export const fictionalObservedSubmission = (observer: EmailOperationObserver) => Effect.gen(function* executeObservedNotification() { const operation = yield* (yield* Email).prepare( { body: { text: "A fictional lifecycle example." }, from: "sender@fictional.invalid", subject: "Fictional notification", to: "recipient@fictional.invalid", }, { executionMetadata: { tenant: "fictional-private-context" }, idempotencyKey: "caller-owned-event", }, ); yield* operation.validate; return yield* operation.send; }).pipe( Effect.provide( observedEmailOperationLayer( defaultedEmailLayer(recordingEmailLayer, { idempotencyKeyPrefix: "fictional/" }), observer, ), ), ); ``` The callback is an `EmailOperationObserver`. It sees the effective operation at this wrapper boundary, including private previews/context and its modeled outcome. Choose wrapper order deliberately: an observer outside defaults sees a different preparation boundary from an observer inside them. ## Preserve the prepared operation Call `prepare`, review the resulting previews and use its validation/send Effects within the same live scope. Do not rebuild the operation after review. Observing does not add permission to alter keys, routes or delivery policy. ## Handle callback failure Callbacks are awaited host Effects and may fail unexpectedly. Their failure cannot make an acknowledged submission unsent or turn uncertainty into replay permission. Preserve defects and interruption instead of selecting an ordinary typed send failure from a compound cause. Complete operation events expose private application data by design. Apply access and retention controls to callback destinations. For bounded payload storage, use [capture](/composition/capture); for minimal status-only facts, use [submission observations](/composition/observations). --- # Message transformations Source: https://email-sdk.corvencore.com/composition/transformations Use root `transformedEmailLayer` to run application-owned message preparation around an `Email` Layer: ```ts import { DateTime, Effect, Redacted } from "effect"; import { Email, transformedEmailLayer } from "@corven/email-sdk"; import { defaultedEmailLayer } from "@corven/email-sdk/defaults"; import { resendEmailLayer } from "@corven/email-sdk/resend"; const account = transformedEmailLayer( defaultedEmailLayer(resendEmailLayer({ apiKey: Redacted.make("fictional-unused-key") }), { idempotencyKeyPrefix: "fictional-notifications/", replyTo: "replies@fictional.invalid", }), ({ message, options }) => Effect.succeed({ ...message, subject: options.executionMetadata?.priority === "urgent" ? `[Action requested] ${message.subject}` : message.subject, }), ); export const fictionalTransformedSubmission = Effect.gen(function* prepareNotification() { const email = yield* Email; const operation = yield* email.prepare( { body: { text: "A fictional transformation example." }, from: "sender@fictional.invalid", subject: "Fictional notification", to: "recipient@fictional.invalid", }, { executionMetadata: { audit: { at: DateTime.makeUnsafe("2026-01-02T03:04:05.123Z"), flags: [0, false, null, undefined], }, priority: "urgent", }, idempotencyKey: "caller-owned-logical-event", }, ); return { preview: operation.previews[0], receipt: yield* operation.send }; }).pipe(Effect.provide(account)); export const fictionalContextualAudience = Effect.gen(function* prepareContextualAudience() { const email = yield* Email; return yield* email.sendPersonalized( { message: { body: { text: "Hello %recipient.name%. Your notification is ready." }, from: "sender@fictional.invalid", subject: "Fictional notification for %recipient.name%", }, recipients: [ { to: "first@fictional.invalid", variables: { name: "First" } }, { to: "second@fictional.invalid", variables: { name: "Second" } }, ], }, { executionMetadata: { priority: "urgent" } }, ); }).pipe(Effect.provide(account)); ``` The transform receives owned message/options and returns replacement message input as an Effect. The example uses private execution context to choose a subject prefix; that context is not automatically forwarded to the provider. ## Preserve operation authority A transform replaces message data, not operation keys or delivery policy. It cannot dispatch after local refusal or convert an observation failure into permission to resend. Its returned message still passes common decoding and provider validation. Supply a closed callback, or an acquisition Effect that captures its dependencies within the Layer through `EmailExtension`. Resources acquired there last for the owning Layer scope; a returned prepared operation does not extend that lifetime. ## Review once Prepare the message after defaults and transformations, review the actual candidates, and send that same prepared operation. Recomputing a transformation after approval can create content that was never approved. This wrapper requires `Individual` personalization and rejects native audiences. For a provider-managed template, use the native job service rather than a transformation that silently changes the sending model. --- # Deadlines and cancellation Source: https://email-sdk.corvencore.com/concepts/cancellation A deadline limits how long the SDK owns an operation. It cannot recall an email already accepted by a provider or prove that an interrupted request was not submitted. ## Delivery deadlines A delivery policy supplies `deadline`, `initialDelay` and `maxAttempts`. The deadline includes the provider's retry sequence. A routed operation has one overall budget rather than a fresh budget for each fallback account. When a submission deadline leaves the outcome unresolved, preserve `EmailDeliveryUnknown`. An attempt count of one avoids repeated attempts but still cannot guarantee non-submission after a timeout. ## Caller interruption Defects and interruption remain Effect causes. Batch operations cancel active work and propagate them rather than converting them into ordinary failed-item Results. Earlier accepted batch items remain accepted. Provide the same cancellation semantics through custom transports and AI runners. A callback failure or interruption must not become a reason to try another account. ## Native work has physical limits Interrupting a Blob attachment read or React render stops awaiting its result. Native work can continue after that interruption. Do not equate an interrupted wait with physical cancellation, secure erasure or absence of side effects. Prepared operations and AI tools depend on their acquiring scope. Keep it open for the full execution, then release it at the owning application boundary. See [AI tools](/integrations/ai) and [React rendering](/integrations/react). --- # Idempotency and retries Source: https://email-sdk.corvencore.com/concepts/idempotency An idempotency key identifies one application operation in a provider account. A retry uses the same key and prepared request. In this documentation, replay means retrying a request whose submission outcome is unknown. ## Read the sender declaration `EmailSenderIdempotencySchema` has three cases: | Case | Meaning | | --------------- | -------------------------------------------------------------------------------------------------- | | `None` | Keys are unsupported and fail local validation. | | `KeyForwarding` | The transport can forward a supported key, but the SDK cannot use it to replay an unknown outcome. | | `Native` | A provider-local replay contract exists for unchanged intent within its retention window. | Declarations can constrain key length, format and whether a key is required. The common key alphabet permits letters, digits, dots, underscores, colons, slashes and hyphens, but a provider can require a narrower alphabet. ## Keep identity stable Persist a key with the logical event before dispatch. Reuse it only for that event, account and unchanged effective content. Include the effects of defaults and transformations when deciding whether intent is unchanged. A different account or route does not inherit the original account's duplicate-prevention guarantee. A new SDK execution does not make an old key new. Your application must account for the provider retention window, process loss and any earlier backup acceptance. ## Check the configured profile JetEmail and Primitive forward keys, but the SDK does not retry their requests after an unknown outcome. SendHeron's key support depends on attachment configuration. Lettr's key support depends on submission mode. Check the [provider guide](/providers) for the configured mode. The provider's idempotency support, the delivery policy and the error reason must all permit the retry. See [receipts and retries](/reliability) and [delivery policy options](/reference/options). --- # Privacy and diagnostics Source: https://email-sdk.corvencore.com/concepts/privacy The SDK does not install an analytics exporter, discover credentials or send import-time telemetry. Applications still need to control their transport logs, callbacks, framework histories and provider settings. ## Separate data channels | Data | Intended destination | | ------------------------------------------------------- | ---------------------------------------------------------------------------- | | Message body, recipients and supported message metadata | The selected provider, as explicit send intent | | `executionMetadata` | Private application preparation, approval and complete operation observation | | Minimal submission observations | Bounded provider/outcome categories without payloads | | Explicit payload capture | Authorized private application storage with retention limits | SDK diagnostics and spans exclude original provider payloads, credentials, keys, message content and original private defects. Safe issue paths help identify a field without echoing its value. Do not add sensitive values to span names, route names or provider labels. ## Control the surrounding runtime Disable automatic redirects, hidden mutation retries and sensitive logging in supplied transports. Review provider-side tracking, copies, rendering, profile writes and retention before making configuration attestations. AI frameworks can retain tool inputs and histories even when the SDK's own output is minimal. Keep secrets and private content out of model context unless your application has explicitly authorized that disclosure. ## Capture is an explicit choice Payload capture requires permission and byte/count bounds. Define access controls and an expiry policy before enabling it. Purging a view does not erase snapshots that escaped to callers or reset replay evidence. Read [capture](/composition/capture) before using it as an audit source. --- # Choose a sending mode Source: https://email-sdk.corvencore.com/concepts/sending-modes Choose the operation that describes your application intent, then check that the configured provider can preserve it. | Intent | Operation | Submission unit | | ----------------------------------- | ------------------------------ | -------------------------------------------------------------------------- | | One literal message | `Email.send` | One prepared message | | Independent messages | `Email.sendAll` | One item per request envelope | | One template with an audience | `Email.sendPersonalized` | Individual messages by default; a native request only with an opt-in Layer | | Provider-managed published template | `LoopsTransactionalEmail.send` | One native template job | | Provider-managed triggered campaign | `IterableCampaignEmail.send` | One native campaign job | ## Batches and personalization Batch Results retain input order. Concurrency controls dispatch, not atomicity: a later failure cannot undo an already accepted item. Every independent logical message needs its own key when the provider supports keys. `Individual` personalization expands the application template before dispatch. `NativeAudience` shares a provider request and its acknowledgment. Native execution is not an automatic optimization; wrappers and routing can refuse it. See [personalization](/guides/personalization). ## Scheduling `sendAt` describes a provider schedule where supported. The SDK does not run a durable background scheduler or persist future work. For an application-owned schedule, retain the job and dispatch it from your worker at the intended time. See [scheduling](/guides/scheduling). ## Review before sending Use `Email.prepare` when the effective message must be reviewed after defaults, transformations or routing. Dispatch that same prepared operation instead of constructing a new message after approval. See [preparation](/composition#prepare-a-message-for-review). --- # Services and Layers Source: https://email-sdk.corvencore.com/concepts/service-model An Effect program declares its service dependencies. A Layer provides those services. Write notification logic using `Email`, then provide a Layer for the selected provider account and transport. ## Email is the application service Acquire `Email` to validate, prepare, send, batch or personalize messages. Its methods return Effects; constructing an Effect does not execute a provider call. Provide an account Layer or an offline recording Layer where your application provides Layers. Common preparation decodes inputs and owns their values so later caller mutation cannot change the submitted intent. Defaults, transformations, routing and approval work with that prepared operation. ## EmailSender `EmailSender` supplies a provider name, local capabilities, an idempotency declaration, local message validation and submission. `emailLayer` builds the common `Email` workflow around that capability. Normal applications use provider convenience Layers such as `resendEmailLayer`. Implement `EmailSender` directly only when authoring an integration whose protocol or configuration does not fit the HTTP provider builder. ## Transport is explicit HTTP account Layers require `HttpClient` until the host provides one. The [sending example](/sending) supplies a client with redirects disabled. SMTP owns an explicit socket/TLS transport instead. Keep resource scopes open for all Effects that use acquired services. A prepared value or AI tool does not extend the lifetime of the Layer that created it. Reuse the same Layer values when a recorder, capture query or observation view must share the sender's acquired store. See [provider authoring](/guides/authoring-provider) and the [sender contract](/reference/sender) for extension boundaries. --- # Credentials and account configuration Source: https://email-sdk.corvencore.com/getting-started/credentials Use your existing provider account. Corven does not issue a platform API key, host an email account or authenticate against the documentation site. ## Acquire secrets in the host Your application reads environment variables, a secret manager or another approved credential source and supplies `Redacted` values to provider Layers. The SDK does not discover `.env` files, cloud identity endpoints or account configuration. `Redacted` reduces accidental disclosure; it does not make a secret safe to put in logs, telemetry, browser bundles or model input. Keep the value wrapped until the trusted request builder needs it. ## Match the provider's option names Credential options differ: Resend uses `apiKey`, Postmark and MailPace use `serverToken`, Lettermint uses `apiToken`, and Cloudflare needs both `apiToken` and `accountId`. SES needs explicit signing credentials; Graph needs an access token acquired and refreshed by your host. SMTP has an explicit authentication union. Select your [provider guide](/providers) for a complete configured Layer example. A token accepted by configuration validation can still lack remote sending permission. ## Review account attestations Some Layers require literal settings such as `tracking: "DomainDisabled"` or `contactWrites: "AllowUpsert"`. These record what behavior your application permits or what remote settings it has checked. They do not cause the SDK to inspect, disable or enable anything remotely. Review these choices before copying an example into production. In particular, profile writes, rendering, retention and automatic copies can change the privacy or content contract. ## Verify without sending Use [local preflight](/providers/capabilities) to check message support without credentials. The [CLI](/reference/cli) is also offline. Neither authenticates an account. Live authentication and sending require separate operational controls; see [verification boundaries](/providers/verification). --- # Installation and runtime Source: https://email-sdk.corvencore.com/getting-started/installation Use Node.js **>=24.18.0** and Effect **4.0.3**. The SDK is currently distributed from a local checkout. Follow the [quickstart](/quickstart#1-install-the-package) to build a tarball and install it into your application. ## Choose an entrypoint Import `Email` and common message types from `@corven/email-sdk`. Import provider Layers from their explicit subpaths, such as `@corven/email-sdk/resend`. Optional rendering, agent and webhook integrations have their own entrypoints; [package reference](/reference) lists their purposes. The package is ESM. On supported Node versions, synchronous CommonJS consumers can load the same ESM graph. There is no separate CommonJS build and no compatibility guarantee for older Node, browsers, Bun or edge runtimes. In particular, an HTTP provider name does not prove that the whole package can run in a Worker. ## Add React rendering Core sending does not require React. For `@corven/email-sdk/react`, install the matching optional peers: ```sh pnpm add react@19.3.1 react-dom@19.3.1 @react-email/render@2.1.0 ``` TypeScript applications using React also need compatible React declarations. See [React rendering](/integrations/react). ## Keep the package boundary intact Use exported subpaths rather than importing internal `dist/` files. Installed declarations describe the exact Effect errors and service requirements. Keep those requirements visible until your application provides the corresponding Layers. --- # Create an HTTP provider Source: https://email-sdk.corvencore.com/guides/authoring-provider Import `defineEmailHttpProvider` from `@corven/email-sdk/provider`. Use it when capabilities, idempotency and local message rules are independent of account configuration. ## Define the protocol This example describes a fictional API that accepts the complete normalized message and acknowledges it with a 202 response containing an ID. It is a contract example, not a ready-made integration for a real service. ```ts import { Effect, Redacted, Schema } from "effect"; import { HttpClientRequest } from "effect/http"; import { EmailDeliveryUnknown, EmailMessageIdSchema, EmailReceiptSchema, EmailRejected, EmailSenderIdempotencySchema, } from "@corven/email-sdk"; import { defineEmailHttpProvider, EmailHttpCredentialSchema, readEmailHttpJson, } from "@corven/email-sdk/provider"; const exampleAcknowledgement = Schema.Struct({ id: EmailMessageIdSchema }); export const exampleHttpProvider = defineEmailHttpProvider({ configuration: Schema.Struct({ apiKey: EmailHttpCredentialSchema }), makeProtocol: ({ apiKey }) => Effect.succeed({ request: (submission) => HttpClientRequest.post("https://mail.example.invalid/messages").pipe( HttpClientRequest.bearerToken(Redacted.value(apiKey)), HttpClientRequest.bodyJson({ message: submission.message }), Effect.mapError( () => new EmailRejected({ provider: "example", reason: "invalid-message" }), ), ), respond: (response) => { if (response.status !== 202) { return Effect.fail( new EmailDeliveryUnknown({ provider: "example", reason: "unexpected-status", status: response.status, }), ); } return readEmailHttpJson(response, "example").pipe( Effect.flatMap(Schema.decodeUnknownEffect(exampleAcknowledgement)), Effect.mapError( () => new EmailDeliveryUnknown({ provider: "example", reason: "invalid-response" }), ), Effect.map(({ id }) => EmailReceiptSchema.cases.Accepted.make({ provider: "example", messageId: id }), ), ); }, }), profile: { capabilities: { attachmentContentTypes: "None", attachments: false, headers: "None", inlineAttachments: false, metadata: false, scheduling: false, tags: { asciiOnly: false, maxCount: 0 }, }, idempotency: EmailSenderIdempotencySchema.cases.None.make({}), provider: "example", validate: () => Effect.void, }, span: "ExampleMail.submit", }); ``` The result exposes `profile`, `preflight`, `senderLayer` and `emailLayer`. The builder enforces declared capabilities before provider-specific validation. Unsupported attachments, headers, metadata, schedules, tags and keys are refused locally here. ## Own configuration and resources Configuration arguments are decoded values validated against the Schema's type side; codec transformations and decoding defaults do not run. Acquire dependencies and resources inside `makeProtocol`, destructure retained configuration and keep credentials Redacted until constructing the request. Returned hooks must have their service requirements resolved. The host supplies an HTTP client without hidden send retries, redirects or private logging. Use static, non-private provider/span labels. ## Classify evidence conservatively Implement response rules for the provider's documented acknowledgments and rejections. Declare native idempotency only when the provider supports it for the selected account and operation. HTTP status, forwarded keys and Retry-After delays are not sufficient for that declaration. Reject unsupported fields. Return an unknown outcome for ambiguous transport or response failures. Leave retries and fallback to SDK policy. For configuration-dependent protocols, SMTP or native jobs, implement the appropriate [sender contract](/reference/sender) rather than forcing them into this builder. Before shipping an extension, test public calls, local refusal, loopback wire behavior and installed consumers. Publication requires your own package/release process; the SDK has no community registry service. --- # Migrate an existing notification workflow Source: https://email-sdk.corvencore.com/guides/migration Migrate one workflow and one account at a time. Corven is an Effect-native redesign, not a source-compatible replacement for another email SDK or Nodemailer. ## List the current message fields Record sender and recipient rules, body formats, attachments, headers, metadata, schedules, tracking, automatic copies and any provider-managed rendering. Preserve existing consent and suppression behavior. Compare that intent with the [provider guide](/providers) and [field-support table](/providers/field-support). A familiar provider name does not mean every upstream SDK option is implemented. ## Configure the Email Layer Keep notification code dependent on `Email`; provide the selected provider account and safe transport in the host. Start with an offline [recording Layer](/guides/testing) to compare caller-visible messages without submitting them. Translate input into the [common message model](/reference/message). For SMTP, supply explicit TLS and authentication settings instead of a Nodemailer transporter. For Loops or Iterable, use the native job service rather than forcing provider templates into a literal-message envelope. ## Translate outcomes, not just method names Replace boolean sent flags with the receipt union and explicit unknown state. Preserve partial results and recipient evidence. Remove transport-level automatic retries before enabling SDK policy. Do not carry a key to a provider that rejects keys, or move a previously attempted event to another account on restart. Review existing queue/recovery behavior against [idempotency](/concepts/idempotency). ## Switch only after application checks Compile against the installed package, test the same application program with recording and controlled failures, and review the configured account attestations. Real authentication, acceptance and delivery remain separate operational checks; compiled examples do not establish them. --- # Personalize an email Source: https://email-sdk.corvencore.com/guides/personalization Use `sendPersonalized` for an explicit template and audience. Templates omit recipient fields; audience entries supply `to`, variables and optional keys. ```ts import { Effect } from "effect"; import { Email, renderEmailPersonalizedMessage } from "@corven/email-sdk"; import { recordingEmailLayer } from "@corven/email-sdk/testing"; const template = { body: { html: "

Hello %recipient.name%

", text: "Hello %recipient.name%" }, from: { address: "sender@example.test", name: "Synthetic Sender" }, subject: "Hello %recipient.name%", } as const; export const personalizedPreview = renderEmailPersonalizedMessage(template, { to: "first@example.test", variables: { name: "Fictional & synthetic" }, }); export const recordedPersonalization = Effect.gen(function* recordPersonalizedNotifications() { return yield* (yield* Email).sendPersonalized( { message: template, recipients: [ { idempotencyKey: "synthetic/first", to: "first@example.test", variables: { name: "First" }, }, { idempotencyKey: "synthetic/second", to: "second@example.test", variables: { name: "Second" }, }, ], }, { concurrency: 2 }, ); }).pipe(Effect.provide(recordingEmailLayer)); ``` The preview helper needs no sender. The recording program expands two individual messages without contacting a provider. ## Template expansion Variables use `%recipient.name%` syntax. Missing variables and invalid expanded messages remain failures. HTML expansion escapes text markup, but does not validate URL/CSS contexts or sanitize arbitrary HTML. Keep templates trusted and validate links in application code. Every independent recipient operation has its own key. Shared options carry concurrency and execution context, not one key for the audience. ## Native grouped requests Use `sendgridPersonalizedEmailLayer` or `mailgunPersonalizedEmailLayer` only when the provider's grouped operation matches your intent: ```ts import { Effect, Layer } from "effect"; import type { Redacted } from "effect"; import { Email } from "@corven/email-sdk"; import { sendgridPersonalizedEmailLayer } from "@corven/email-sdk/sendgrid"; import { mailgunPersonalizedEmailLayer } from "@corven/email-sdk/mailgun"; import { emailHttpLayer } from "./sending.js"; export const sendgridNativeAccount = (apiKey: Redacted.Redacted) => sendgridPersonalizedEmailLayer({ apiKey, region: "Europe" }).pipe(Layer.provide(emailHttpLayer)); export const mailgunNativeAccount = (apiKey: Redacted.Redacted, domain: string) => mailgunPersonalizedEmailLayer({ apiKey, domain, region: "Europe" }).pipe( Layer.provide(emailHttpLayer), ); export const submitNativeAudience = Effect.gen(function* submitNativeAudience() { const email = yield* Email; return yield* email.sendPersonalized({ message: { body: { html: "

Hello %recipient.name%

", text: "Hello %recipient.name%" }, from: "sender@example.test", subject: "Hello %recipient.name%", }, recipients: [ { to: "first@example.test", variables: { name: "First" } }, { to: "second@example.test", variables: { name: "Second" } }, ], }); }); ``` Provide one of these account Layers to `submitNativeAudience`. Inspect `personalization: "NativeAudience"` on the acquired service. A valid subset can be submitted as one request with shared acknowledgment, deadline and correlation. Common defaults, transformations and ordinary routing refuse native-audience Layers. They do not silently rerender or split the request. A per-audience Result is not proof of a separate wire send or independent recipient acceptance. See [sending modes](/concepts/sending-modes) and [batching and routing](/batching-routing) for ordering and failure semantics. --- # Build a production send pipeline Source: https://email-sdk.corvencore.com/guides/production-pipeline A production pipeline needs application-owned state around `Email`. The SDK supplies preparation and submission policy; it does not persist a queue or guarantee exactly-once delivery. ## 1. Record the logical event Persist an event identity, authorized recipients/content and the intended provider account. Store a stable scoped key only when that account supports it. Keep secrets outside queue payloads and define retention for private message data. ## 2. Prepare the effective operation Acquire the configured Layer and call `prepare`. Review the candidate previews after defaults, transformations and routing. If human or host approval is required, approve these values and dispatch the same operation while its scope remains open. Local validation catches unsupported intent. It does not authenticate the account or lock remote capacity. Time-sensitive constraints can fail later without making it acceptable to change approved intent. ## 3. Dispatch within a bounded policy Use a complete delivery policy and a transport without hidden retries or redirects. Only eligible definite rejection can advance fallback. Native replay support belongs to one provider account and unchanged request, not to the queue record alone. ## 4. Retain the actual outcome Persist the complete receipt or an explicit uncertain state. Preserve partial recipient evidence, count-only outcomes, suppression and the actual routed account. A message ID is a correlation handle, not a delivered flag. ## 5. Reconcile asynchronously Apply authenticated or explicitly unverified [webhook reports](/integrations/webhooks) under your ingress trust policy. Keep tenant binding and event replay storage durable. An absent report does not establish non-submission, and a duplicate report must not trigger another send. On process recovery, resume from retained account/key/outcome evidence. Restarting every job at the primary after a previous backup acceptance can duplicate email. --- # Compare providers for a workflow Source: https://email-sdk.corvencore.com/guides/provider-comparisons Choose a provider using the message and recovery behavior your application needs. Pricing, account entitlements and remote service capabilities change independently of the SDK; this guide compares implemented local contracts, not current subscription offers. ## Resend and Postmark [Resend](/providers/resend) supports scheduling and native scoped keys, but not common metadata. [Postmark](/providers/postmark) supports metadata and repeated headers, but rejects scheduling and send keys. A keyed Resend workflow is therefore not a drop-in Postmark fallback. ## SendGrid and Mailgun Both support rich ordinary messages and opt-in native personalization. [SendGrid](/providers/sendgrid) uses unique headers and a regional JSON protocol; [Mailgun](/providers/mailgun) preserves repeated headers and uses a domain-scoped multipart protocol. Their scheduling precision, tag rules and inline attachment constraints differ. Neither adapter declares native send replay authority. ## SES and Microsoft Graph [SES](/providers/ses) uses explicit regional signing credentials and supports bounded ASCII tags. [Graph](/providers/graph) uses a host-acquired access token and mailbox identity, with one representable body format and no common tag support. Both require reviewed mail-flow authority and provide no send key support here. ## Loops and literal-message providers [Loops](/providers/loops) runs a provider-managed published template. It is not a substitute account for a literal-message route. Compare native content/profile behavior and ownership before deciding that two methods named `send` represent the same operation. ## SMTP and HTTP providers [SMTP](/providers/smtp) supplies its own explicit TLS transport and protocol-level recipient evidence. HTTP providers require a host HTTP client and provider-specific acknowledgment classification. Neither transport family automatically proves delivery or safe replay. Before routing between any pair, check [field support](/providers/field-support), then preflight every selected candidate's effective message, key and policy. A compatible message format alone is insufficient for fallback authority. --- # Schedule an email Source: https://email-sdk.corvencore.com/guides/scheduling Use `sendAt` when the selected provider supports scheduling and its time window fits your job. Use an application-owned scheduler when work must survive restarts, be dispatched outside that window or be controlled by your queue. ## Supply a future instant ```ts import { DateTime, Effect } from "effect"; import { Email } from "@corven/email-sdk"; export const scheduleNotification = Effect.gen(function* scheduleNotification() { const email = yield* Email; const sendAt = DateTime.add(yield* DateTime.now, { minutes: 10 }); return yield* email.send({ body: { text: "A scheduled notification." }, from: "sender@example.test", sendAt, subject: "Notification", to: "recipient@example.test", }); }); ``` Provide a compatible `Email` Layer before running the program. This example computes a future instant when the Effect executes. It does not reserve provider capacity or persist the schedule. ## Validate precision and lead time The common input accepts a native `Date`, an Effect UTC instant or a strict timestamp with seconds and a known offset. Provider rules can require whole seconds, limit how far ahead the instant may be, or restrict scheduling to a configured mode. The ten-minute example is not portable to every provider. Inspect [field support](/providers/field-support) and the selected provider page. Recheck close to dispatch and allow for preparation, latency and clock skew. Do not silently round, advance or drop an expired schedule. ## Own durable scheduling For an application queue, retain message data, the logical event identity and the selected account. When the worker executes, prepare and validate the message again within its authorized policy. If you use provider scheduling, handle its acknowledgment as scheduled submission evidence, not completed delivery. Cancellation of an SDK Effect is not cancellation of an already accepted remote schedule. This SDK does not expose a universal provider cancellation operation. --- # Test email workflows Source: https://email-sdk.corvencore.com/guides/testing Replace the provider Layer where your application provides Layers. Keep the notification program unchanged so tests exercise the same `Email` calls as production. ## Inspect recorded messages Use `recordingEmailLayer` and acquire `RecordedEmails` in the same scope, as in the [quickstart](/quickstart). Inspect the returned messages and receipt. Assert the expected recipient, rendered content or absence of a submission after a refused action. A recorder is in-memory simulation. Reacquisition creates a separate store; purging visible records is not a durable duplicate-prevention mechanism. ## Simulate uncertainty ```ts import { Duration, Effect } from "effect"; import { Email, EmailDeliveryUnknown } from "@corven/email-sdk"; import { FailedEmailSubmissions, failingEmailLayer } from "@corven/email-sdk/testing"; export const inspectUncertainSubmission = Effect.gen(function* inspectUncertainSubmission() { const email = yield* Email; const failures = yield* FailedEmailSubmissions; const result = yield* email .send({ body: { text: "Your notification is ready." }, from: "sender@example.test", subject: "Notification", to: "recipient@example.test", }) .pipe(Effect.result); return { attempts: yield* failures.attempts, result }; }).pipe( Effect.provide( failingEmailLayer(new EmailDeliveryUnknown({ provider: "example", reason: "transport" }), { deadline: Duration.seconds(10), initialDelay: Duration.millis(100), maxAttempts: 3, }), ), ); ``` `failingEmailLayer` exposes `FailedEmailSubmissions` and a controlled expected error. This sender does not support idempotent retries after an unknown outcome. The program records one attempt even though its policy permits up to three. Use this result to test your application's pending/reconciliation state. Do not convert it to an unsent flag or route it automatically to a backup. To exercise eligible rejection behavior instead, supply an `EmailRejected` with the appropriate reason and explicit policy. ## Choose meaningful assertions Test observable messages, results, route choices and side effects through public services. Avoid factory-count assertions, copied constants and snapshots whose only purpose is to detect edits. Add a regression case only when it fills a genuine behavior gap. For time and ordering tests, use the Effect test clock and explicit coordination rather than sleeps. Separate application simulation from loopback protocol tests and [live provider evidence](/providers/verification). --- # Troubleshoot a failed send Source: https://email-sdk.corvencore.com/guides/troubleshooting Start with the tagged error or receipt, not a generic transport exception. Preserve the provider, safe reason/category and issue paths while excluding credentials, recipient lists, content and original private causes from logs. ## Layer acquisition fails `EmailConfigurationError` means no usable service was acquired. Check the provider's option names, required attestations and typed credential format. An unsupported wrapper around `NativeAudience` also fails here. Correct configuration before acquiring a new Layer. ## Input is refused locally For `EmailInvalidMessage` or `EmailInvalidOptions`, inspect safe issue paths. Check the [message contract](/reference/message), [options](/reference/options) and provider-specific constraints. Handle unsupported fields, schedules and keys as validation errors. Change them only through an explicit application decision. Use `corven-email validate` for common JSON input or a supported provider profile for credential-free checks. Passing these commands does not prove account authentication. ## The provider rejected the request `EmailRejected` is definite rejection evidence. Authentication failures need account/credential correction. Invalid messages need input correction. Conflicts remain terminal. Only reasons eligible under the configured SDK policy permit retry or fallback. ## The outcome is unknown For `EmailDeliveryUnknown`, retain a pending/uncertain state and reconcile. A timeout, invalid acknowledgment or server error is not an unsent result. Do not change accounts or keys to work around uncertainty. See [idempotency](/concepts/idempotency). ## An Effect was interrupted or defected Keep the full cause semantics. Do not settle it as an ordinary failed batch item or select one typed failure from a compound cause to make it retryable. Observation failures cannot undo acceptance. If behavior differs only in your installed application, check the exact runtime, Effect peer, package entrypoint and optional framework declarations. See the [error reference](/reference/errors) for field-level interpretation. --- # Integrations Source: https://email-sdk.corvencore.com/integrations Choose an integration by where it sits in your workflow. Rendering produces a body before submission; agent tools request host-approved dispatch; webhook processing receives provider reports afterward. ## React rendering [Render trusted React email](/integrations/react) into HTML and plain text, then pass that body through ordinary provider validation and policy. Use [components and themes](/ui) for layouts and task-specific application recipes. Optional rendering peers are isolated from core sending. Rendering is not sanitization, sending or a guarantee that every email client displays the same result. ## Host-approved agent and AI tool [EmailAgent](/integrations/agent) restricts model proposals while the host owns sender, account, keys and approval. [emailAiTool](/integrations/ai) bridges that service into a scoped Standard Schema tool without acquiring a model client. Keep the tool's scope open through execution. Model confirmation and framework approval flags never replace the host's decision over the effective prepared candidates. ## Inbound reports The [webhook overview](/integrations/webhooks) separates verification, event projection, tenant binding and durable event replay. Use provider-specific guidance: - [Resend](/integrations/webhooks/resend): raw-body/header verification before projection. - [Mailgun](/integrations/webhooks/mailgun): timestamp/token proof, with event-body content still unverified. - [Postmark](/integrations/webhooks/postmark): unsigned report projection under host-owned ingress authentication. No inbound report grants permission to resend or bypass suppression. For platform integrations not provided by this SDK, keep queues, authentication and durable state in your application; see [scope and compatibility](/reference/scope). --- # Approve agent-requested email Source: https://email-sdk.corvencore.com/integrations/agent Use `EmailAgent` from `@corven/email-sdk/agent` to expose a constrained proposal boundary. Model input contains `to`, `subject` and `body`. The host supplies the sender and the configured `Email` service. ```ts import { Effect, Layer } from "effect"; import type { Redacted } from "effect"; import { EmailAgent, EmailAgentApproval, emailAgentLayer } from "@corven/email-sdk/agent"; import type { IEmailAgentApproval } from "@corven/email-sdk/agent"; import { postmarkEmailLayer } from "@corven/email-sdk/postmark"; export const hostApprovedAgentLayer = ( serverToken: Redacted.Redacted, review: IEmailAgentApproval["review"], ) => emailAgentLayer({ from: { address: "host@fictional.invalid", name: "Fictional host" } }).pipe( Layer.provide(Layer.succeed(EmailAgentApproval, EmailAgentApproval.of({ review }))), Layer.provide(postmarkEmailLayer({ serverToken })), ); export const fictionalAgentSubmission = Effect.gen(function* submitHostReviewedNotification() { const agent = yield* EmailAgent; return yield* agent.submit({ body: { text: "Fictional content for the host to review." }, subject: "Fictional agent notification", to: "recipient@fictional.invalid", }); }); ``` The account Layer still needs HTTP transport. The sample accepts a host review callback rather than automatically approving proposals. ## Review effective candidates `EmailAgentApproval.review` receives owned `message`, `options` and all effective `previews`. Defaults, transformations and routes have already been prepared, so review the actual candidates rather than the model's earlier draft. Return `Approved` only when host policy permits that exact operation. `Declined` fails with `EmailAgentApprovalError`; unavailable review must remain an explicit failure. The SDK validates and dispatches the same prepared operation after approval. ## Keep authority in the host Do not accept model-supplied sender accounts, credentials, route policy or replay keys as authority. Framework approval flags, confirmation text and tool-call IDs do not replace `EmailAgentApproval`. Approval does not prove account permission, submission or delivery. A failed or interrupted tool call can leave the outcome unknown, so do not retry automatically. Use [AI tools](/integrations/ai) for a framework bridge and [prepared operations](/composition) for scope and callback semantics. --- # Add an AI email tool Source: https://email-sdk.corvencore.com/integrations/ai `emailAiTool` from `@corven/email-sdk/ai` creates an Effect requiring `EmailAgent` and `Scope`. It does not acquire a model client or provider account. ```ts import { Effect } from "effect"; import { emailAiTool } from "@corven/email-sdk/ai"; export const hostAiTools = emailAiTool(Effect.runPromise).pipe( Effect.map((sendEmail) => ({ sendEmail })), ); ``` Compose the [host-approved agent Layer](/integrations/agent) and keep the tool's owning scope open until all executions finish. Do not return a tool from a scope that is immediately closed. ## Runner contract Supply an `EmailAiRunner` that runs the provided Effect and forwards its abort signal. The runner returns a Promise-like result. Propagate failures and cancellation. Do not report an unknown outcome as success or retry it in the runner. The tool exposes `description`, `inputSchema`, `outputSchema` and `execute`. Declarations use Standard Schema and Standard JSON Schema rather than importing an AI SDK. Check your exact framework version in the consuming application; successful runtime execution does not prove framework-native type compatibility. ## Results and privacy Output contains the receipt kind. It does not report delivery. `EmailAiToolError` reports `defect` or `not-completed` without exposing private original failures. A failed/interrupted call is not permission to resend. Framework conversation histories and telemetry can retain proposed recipients and content. Apply application privacy controls and keep secrets out of model context. Host approval remains mandatory even if the framework displays its own confirmation UI. --- # Render React email Source: https://email-sdk.corvencore.com/integrations/react Import `renderReactEmail`, `reactEmailTheme` and `themedReactEmailComponents` from `@corven/email-sdk/react`. Install the [matching optional peers](/getting-started/installation#add-react-rendering); core sending and the CLI do not require React. ```ts import { Effect } from "effect"; import { createElement } from "react"; import { Email } from "@corven/email-sdk"; import { reactEmailTheme, renderReactEmail, themedReactEmailComponents, } from "@corven/email-sdk/react"; const { EmailAction, EmailCard, EmailDivider, EmailDocument, EmailHeading, EmailParagraph } = themedReactEmailComponents( reactEmailTheme("light", { accent: "#234c39", fontFamily: "Georgia, serif" }), ); export const submitRenderedReactExample = Effect.gen(function* submitRenderedReactExample() { const body = yield* renderReactEmail( createElement( EmailDocument, { lang: "en", preview: "A synthetic component-catalog example." }, createElement( EmailCard, null, createElement(EmailHeading, null, "Fictional notification"), createElement(EmailParagraph, null, "Fictional content rendered before submission."), createElement(EmailAction, { href: "https://example.invalid/review" }, "Review example"), createElement(EmailDivider), createElement( EmailParagraph, { tone: "muted" }, "Synthetic content, not a customer message.", ), ), ), ); const email = yield* Email; return yield* email.send({ body, from: "sender@example.invalid", subject: "Synthetic React email example", to: "recipient@example.invalid", }); }); ``` The program renders a body, then uses ordinary `Email.send`. Provide an `Email` Layer before execution. Rendering itself does not submit email. ## Output and failures `renderReactEmail(element)` returns an Effect containing a frozen `{ html, text }` body. It renders HTML, derives plain text and validates the common body contract. `EmailRenderError.reason` is `invalid-template`, `render-failed` or `invalid-output`; failures do not expose original renderer diagnostics. ## Trusted application code Components are host code and can run their own side effects. Rendering does not sanitize untrusted HTML, approve recipients or validate URL schemes. Build templates from trusted elements and authorized data; apply link and privacy rules in the host. Interruption stops awaiting the native renderer, but cannot physically cancel component work. Keep that limit in request and approval deadlines. Use the [component reference](/ui/components), [theme guide](/ui/themes) and [template recipes](/ui) for application layouts. Always preflight the resulting body for the selected provider; rendering success does not establish provider acceptance or email-client compatibility. --- # Process webhook reports Source: https://email-sdk.corvencore.com/integrations/webhooks Provider reports can help reconcile submission state, but their proof scopes differ. Select the integration that matches the provider and the evidence your application actually receives. | Provider | SDK support | Trust boundary | | ------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------ | | [Resend](/integrations/webhooks/resend) | Raw-body/header verification and event projection | Projection consumes verified request evidence | | [Mailgun](/integrations/webhooks/mailgun) | Timestamp/token signature verification and separate event projection | The signature does not authenticate event-body content | | [Postmark](/integrations/webhooks/postmark) | Unsigned event projection | Ingress authentication remains application-owned | ## Receive the request safely Acquire the raw body and complete required headers before lossy middleware transforms them. Preserve duplicate-header evidence where the verifier needs it. Keep secrets, payloads and original parser failures out of diagnostics. Bind the request to the intended tenant/account using trusted ingress, not an unsigned account field in the payload. Configure key rotation and freshness within the actual provider verification contract. ## Apply events idempotently Persist replay state and event application decisions in your application. Freshness is not uniqueness; a valid signature does not prove that the event is new or represents the latest delivery state. A projected Delivered event is a provider report, not a universal inbox-placement guarantee. Parsing a serialized observation Schema does not recreate authentication. No webhook event authorizes resending, bypassing suppression or changing consent. For outbound state, retain [receipts and uncertainty](/reliability) separately from inbound reports. --- # Mailgun webhooks Source: https://email-sdk.corvencore.com/integrations/webhooks/mailgun Import the signature verifier and event parser from `@corven/email-sdk/mailgun-webhook`: ```ts import { Effect } from "effect"; import type { Redacted } from "effect"; import { MailgunWebhookEventSchema, MailgunWebhookVerifier, mailgunWebhookLayer, parseMailgunWebhookEvent, } from "@corven/email-sdk/mailgun-webhook"; import type { MailgunWebhookSignature } from "@corven/email-sdk/mailgun-webhook"; export const verifyIncomingMailgunSignature = Effect.fnUntraced( function* verifyIncomingMailgunSignature(signature: MailgunWebhookSignature) { const verifier = yield* MailgunWebhookVerifier; return yield* verifier.verifySignature(signature); }, ); export const parseIncomingMailgunReport = Effect.fnUntraced(function* parseIncomingMailgunReport( rawBody: string, ) { const event = yield* parseMailgunWebhookEvent(rawBody); const category = MailgunWebhookEventSchema.match(event, { Complained: () => "ComplaintReport" as const, Delivered: () => "RecipientServerAcceptanceReport" as const, Failed: ({ severity }) => severity === "permanent" ? ("PermanentFailureReport" as const) : ("TemporaryFailureReport" as const), Other: () => "UnclassifiedReport" as const, }); return { category, provenance: event.provenance }; }); export const incomingMailgunWebhookLayer = (signingKey: Redacted.Redacted) => mailgunWebhookLayer({ signatureField: "signature", signingSecrets: [signingKey] }); ``` ## Verify signature evidence `MailgunWebhookVerifier.verifySignature` checks timestamp/token evidence with the configured Redacted signing keys and freshness tolerance. Select `signatureField` explicitly as `signature` or `parent-signature`; the appropriate choice depends on your ingress contract. The proof covers the signature fields only. Event-body content remains unverified. Store the event with that provenance, even if signature verification succeeds. ## Project event reports `parseMailgunWebhookEvent(rawBody)` returns supported Delivered, Failed, Complained or Other projections. Failures retain severity where supplied. Projected event provenance remains `UnverifiedPayload`. Your host must acquire and authenticate the appropriate ingress, bind the account/tenant and retain durable replay state before applying a report. A valid signature, a body account field or a fresh timestamp alone is insufficient. A recipient-server acceptance report is not permission to send again. Keep event interpretation separate from [outbound recovery policy](/reliability). --- # Postmark webhook reports Source: https://email-sdk.corvencore.com/integrations/webhooks/postmark Use `parsePostmarkWebhookEvent` and `PostmarkWebhookEventSchema` from `@corven/email-sdk/postmark-webhook`: ```ts import { Effect } from "effect"; import { PostmarkWebhookEventSchema, parsePostmarkWebhookEvent, } from "@corven/email-sdk/postmark-webhook"; export const parseIncomingPostmarkReport = Effect.fnUntraced(function* parseIncomingPostmarkReport( rawBody: string, ) { const event = yield* parsePostmarkWebhookEvent(rawBody); const category = PostmarkWebhookEventSchema.match(event, { BounceReported: () => "NativeBounceReport" as const, Bounced: () => "HardBounceReport" as const, Complained: () => "SpamComplaintReport" as const, Delivered: () => "RecipientServerAcceptanceReport" as const, Other: () => "UnclassifiedReport" as const, }); return { category, provenance: event.provenance }; }); ``` ## Understand the projection Supported cases include Delivered, Complained, Bounced for a hard bounce, BounceReported for other supported bounce reports, and Other. Bounce type/code consistency is checked; do not collapse every native bounce report into a hard-bounce claim. All projections retain `UnverifiedPayload` provenance. This entrypoint does not provide a request signature verifier. Your application owns ingress authentication, tenant binding, raw-body acquisition and event replay storage. ## Apply reports carefully Treat the event as a provider-shaped report under the trust your ingress establishes. Parsing alone does not establish that Postmark sent it, that the event is new or that it reflects the latest recipient state. Preserve correlation and recipient evidence without logging private payloads. A bounced, complained or suppressed recipient must not be routed automatically to another account. See the [webhook overview](/integrations/webhooks) for application replay and state ownership. --- # Resend webhooks Source: https://email-sdk.corvencore.com/integrations/webhooks/resend Use `ResendWebhookVerifier`, `resendWebhookLayer` and `parseResendWebhookEvent` from `@corven/email-sdk/resend-webhook`. ```ts import { Effect } from "effect"; import type { Redacted } from "effect"; import { ResendWebhookVerifier, resendWebhookLayer, parseResendWebhookEvent, } from "@corven/email-sdk/resend-webhook"; import type { ResendWebhookRequest } from "@corven/email-sdk/resend-webhook"; export const authenticateResendWebhook = Effect.fnUntraced(function* authenticateResendWebhook( request: ResendWebhookRequest, ) { const verifier = yield* ResendWebhookVerifier; return yield* verifier.verify(request); }); export const parseIncomingResendEvent = Effect.fnUntraced(function* parseIncomingResendEvent( request: ResendWebhookRequest, ) { const proof = yield* authenticateResendWebhook(request); return yield* parseResendWebhookEvent(proof); }); export const incomingResendWebhookLayer = (signingSecret: Redacted.Redacted) => resendWebhookLayer({ signingSecrets: [signingSecret] }); ``` ## Supply the original request `ResendWebhookRequest` contains a raw `body` and an array of `{ name, value }` headers. Preserve the body exactly and retain duplicate-header evidence; rebuilding JSON or collapsing headers before verification can destroy the signed contract. The verifier requires the Svix ID, timestamp and signature headers. Configuration accepts a bounded nonempty list of Redacted `whsec_` signing secrets for rotation and optional freshness tolerance. Use webhook signing secrets, not an outbound API key. ## Project verified evidence Call `verify` first, then pass its `VerifiedResendWebhook` proof to `parseResendWebhookEvent`. Event normalization can still fail after signature verification if supported payload structure is invalid. A public observation Schema is not a replacement for this proof path. Authentication does not bind your application tenant or persist event uniqueness. Own those decisions at ingress and apply reports under durable replay control. Verification failure must not trigger a send or turn the event into trusted delivery state. --- # Messages and attachments Source: https://email-sdk.corvencore.com/messages A message requires `from`, `to`, `subject` and `body`. The body must contain nonempty `text`, `html`, or both. Optional fields include `cc`, `bcc`, `replyTo`, headers, tags, metadata, attachments and `sendAt`. The common Schema validates message structure. The selected provider must also support every supplied field; see [providers and validation](/providers). ## Build a message `parseEmailMessage` validates common input and creates an owned message: subsequent changes to caller-supplied objects do not change the decoded message. It does not acquire a sender or run provider-specific checks. ```ts import { Effect } from "effect"; import { emailAttachmentFromBytes, parseEmailMessage } from "@corven/email-sdk"; export const prepareNotification = Effect.gen(function* prepareNotification() { const attachment = yield* emailAttachmentFromBytes( new TextEncoder().encode("Fictional receipt"), { contentType: "text/plain", filename: "receipt.txt" }, ); return yield* parseEmailMessage({ attachments: [attachment], body: { html: "

Your receipt is attached.

", text: "Your receipt is attached." }, from: { address: "sender@example.test", name: "Example Shop" }, subject: "Receipt", to: "Example Customer ", }); }); ``` Pass the result through `email.validate` or a provider preflight function to check provider support before submission. ## Specify addresses Mailbox inputs accept: - A conservative ASCII address, such as `recipient@example.test`. - A structured record with `address` and optional `name`. - A single named mailbox string, such as `Example Customer `. Recipient fields accept one mailbox or a statically nonempty array. Unicode display names are supported, but internationalized mailbox local parts and unrestricted RFC address-list syntax are not. ## Add attachments Use `emailAttachmentFromBytes`, `emailAttachmentFromArrayBuffer` or `emailAttachmentFromBlob` to construct a bounded attachment from bytes your application has already acquired. These helpers do not read file paths, fetch URLs or infer MIME types from filenames or `Blob.type`. Choose an explicit MIME type where the provider supports it. Select `ProviderSelected` only when you intend to delegate MIME selection to a compatible provider. Inline/CID placement also requires provider support. Your application owns file or URL acquisition, content inspection, consent and retention. Attachment byte limits do not guarantee acceptance under the provider's encoded message-size limit. Interrupting a Blob read stops awaiting its result; it cannot guarantee physical cancellation of the native read. ## Schedule submission `sendAt` accepts a native `Date`, an Effect UTC instant, or a strict timestamp containing seconds and a known UTC offset. Decoding preserves one UTC instant and rejects malformed dates, ambiguous offsets or unsupported precision. A parsed schedule is not a provider reservation. Providers may restrict the allowed time window or require whole-second precision. Dispatch can recheck time-sensitive constraints after preparation or queueing, so allow enough lead time and handle expired schedules explicitly. ## Separate provider metadata from host context Use `message.metadata` for provider-facing data when the provider supports it. Use `options.executionMetadata` for bounded private application context needed during preparation, approval or complete operation observation. The SDK does not automatically forward this context to providers, models or minimal observations. It is not a template-variable source, credential store or replay token. --- # Choose a provider Source: https://email-sdk.corvencore.com/providers Choose the provider whose account your application already owns, then check that it can preserve the complete message and recovery policy. Changing Layers keeps the common `Email` interface; it does not make provider features interchangeable. Start with [field support](/providers/field-support), [local preflight](/providers/capabilities) and [verification boundaries](/providers/verification). Each guide below includes a configured Layer example, message constraints and replay limits. ## Ordinary email Layers | Provider | Configuration focus | | ------------------------------------------------- | ------------------------------------------------------------ | | [Resend](/providers/resend) | Native scoped keys and scheduling | | [Postmark](/providers/postmark) | Message streams, repeated headers and metadata | | [SendGrid](/providers/sendgrid) | Regional Mail Send and opt-in native personalization | | [Mailgun](/providers/mailgun) | Domain-scoped multipart messages and repeated headers | | [MailerSend](/providers/mailersend) | Provider-selected attachment MIME and suppression evidence | | [MailPace](/providers/mailpace) | Supported headers, attachment extensions and tags | | [Unosend](/providers/unosend) | Explicit attachment MIME and scheduling | | [Lettermint](/providers/lettermint) | Provider routes, native keys and sandbox-aware receipts | | [Plunk](/providers/plunk) | Contact upserts and provider-generated text | | [Helo](/providers/helo) | Live/sandbox channels and short-lived native replay | | [JetEmail](/providers/jetemail) | Transactional tokens, domain attestations and forwarded keys | | [Lettr](/providers/lettr) | Immediate/scheduled modes and count-only receipts | | [SparkPost](/providers/sparkpost) | Regional transmissions and count-aware acknowledgment | | [Primitive](/providers/primitive) | Explicit key requirements and content deduplication policy | | [Mailtrap](/providers/mailtrap) | Email Sending and multiple acknowledgment IDs | | [Mailchimp Transactional](/providers/mailchimp) | Account behavior and per-recipient outcomes | | [Scaleway](/providers/scaleway) | Project-bound transactional intent | | [ZeptoMail](/providers/zeptomail) | Live or sandbox mail agents | | [Cloudflare Email Sending](/providers/cloudflare) | REST account transport and constrained mail flow | | [Amazon SES](/providers/ses) | Explicit regional SigV4 credentials | | [Microsoft Graph](/providers/graph) | Host-acquired access token and mailbox identity | | [SMTP](/providers/smtp) | Explicit TLS transport and protocol recipient evidence | | [Brevo](/providers/brevo) | Provider rendering and tracking authority | | [Sequenzy](/providers/sequenzy) | Transactional HTML and provider-managed profiles | | [SendHeron](/providers/sendheron) | Attachment policy and configuration-dependent replay | ## Native provider jobs [Loops](/providers/loops) triggers a provider-managed published template. [Iterable](/providers/iterable) triggers a provider-managed campaign. These have separate service interfaces and native `{ job, options? }` batches. They are not ordinary `Email` Layers or fallback targets. ## Compose accounts safely Use [named routing](/batching-routing#route-explicit-accounts) to bind explicit account Layers. Preflight every selected candidate without dropping fields. Only eligible definite rejection advances fallback; unknown or terminal outcomes stop the chain. Opt-in SendGrid/Mailgun `NativeAudience` Layers share one provider request. Common individual-preparation wrappers and ordinary routing refuse that mode. See [personalization](/guides/personalization) before composing grouped sends. --- # Brevo Source: https://email-sdk.corvencore.com/providers/brevo Import `brevoEmailLayer` from `@corven/email-sdk/brevo` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { brevoEmailLayer } from "@corven/email-sdk/brevo"; import { emailHttpLayer } from "./sending.js"; export const configuredBrevoAccount = (apiKey: Redacted.Redacted) => brevoEmailLayer({ apiKey, contentAuthority: "AuthorizeProviderRendering", submissionMode: "TransactionalOnly", tracking: "AuthorizeProviderTracking", }).pipe(Layer.provide(emailHttpLayer)); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - Configure the application to permit provider rendering and tracking. The SDK does not inspect remote account settings. - Attachments use ProviderSelected MIME selection. Inline placement, common metadata and tags are unsupported. - Use one reply identity. Display names and scheduling must pass Brevo-specific validation. ## Idempotency Idempotency keys are not supported. The SDK does not retry requests with unknown submission outcomes. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # Capabilities and local preflight Source: https://email-sdk.corvencore.com/providers/capabilities Use `email.capabilities` to inspect the configured service, then call `email.validate` to apply preparation and the actual provider rules. A capability flag is a local support declaration, not a statement about account permission or remote acceptance. ## Preflight without an account Selected provider subpaths expose credential-free functions: ```ts import { Effect } from "effect"; import { preflightPostmarkEmail } from "@corven/email-sdk/postmark"; import { preflightResendEmail } from "@corven/email-sdk/resend"; const message = { body: { text: "A fictional credential-free preflight example." }, from: "sender@fictional.invalid", subject: "Fictional notification", to: "recipient@fictional.invalid", }; export const fictionalLocalPreflight = Effect.gen(function* inspectFictionalIntent() { const resend = yield* preflightResendEmail(message, { idempotencyKey: "caller-owned-event" }); const postmark = yield* preflightPostmarkEmail(message); return { postmark, resend }; }); ``` This example validates the same message for Resend and Postmark, but supplies a key only to Resend. Giving Postmark an unsupported key must remain a local refusal. A preflight function needs neither credentials nor an HTTP client. It does not authenticate, inspect account settings, reserve a schedule or prevent a later remote rejection. The [CLI providers command](/reference/cli/providers) lists the available offline profiles. ## Preserve complete intent The [field-support table](/providers/field-support) describes base profiles. Provider validation can add restrictions on mailbox formatting, body combinations, MIME types, tag syntax, header names, schedules and recipient evidence. Do not delete a rejected field to force portability. Decide in application code whether a different message is acceptable, then prepare and approve that new intent explicitly. ## Routed candidates Routing preflights every selected candidate's effective message, key and policy before transport. A compatible primary does not excuse an incompatible backup. The acquired routed capabilities describe common support, but they do not replace candidate validation or create cross-account replay guarantees. --- # Cloudflare Email Sending Source: https://email-sdk.corvencore.com/providers/cloudflare Import `cloudflareEmailLayer` from `@corven/email-sdk/cloudflare` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { cloudflareEmailLayer } from "@corven/email-sdk/cloudflare"; import { emailHttpLayer } from "./sending.js"; export const configuredCloudflareAccount = (apiToken: Redacted.Redacted) => cloudflareEmailLayer({ apiToken, accountId: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", mailFlow: "ProviderUnmodified", }).pipe(Layer.provide(emailHttpLayer)); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - This SDK entrypoint uses the REST API with accountId and apiToken. It does not implement a Workers email binding or inbound Email Routing. - mailFlow attests to unmodified provider mail flow. Messages use constrained plain mailbox intent and one reply-to address. - Custom headers are restricted; metadata, tags and scheduling are unsupported. Explicit MIME types and inline attachments are available. - Queued, delivered and suppressed provider evidence is checked for coverage, but the resulting receipt is still submission evidence rather than inbox delivery. ## Idempotency Idempotency keys are not supported. The SDK does not retry requests with unknown submission outcomes. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # Message field support Source: https://email-sdk.corvencore.com/providers/field-support Use this table to shortlist providers, then validate the complete message against the configured Layer. It describes the SDK's exported base capability profiles, not every feature advertised by the remote service. Additional provider rules can refuse an otherwise supported field. | Provider | Headers | Attachment MIME selection | Inline | Metadata | Scheduling | Maximum tags | | ------------------------------------------------- | -------- | ------------------------- | ------ | -------- | ---------- | ------------ | | [Resend](/providers/resend) | Unique | Both | Yes | No | Yes | 75 | | [Postmark](/providers/postmark) | Repeated | Explicit | Yes | Yes | No | 1 | | [SendGrid](/providers/sendgrid) | Unique | Both | Yes | Yes | Yes | 10 | | [Mailgun](/providers/mailgun) | Repeated | Explicit | Yes | Yes | Yes | 3 | | [MailerSend](/providers/mailersend) | Repeated | ProviderSelected | Yes | No | Yes | 5 | | [MailPace](/providers/mailpace) | Unique | Explicit | Yes | No | No | 75 | | [Unosend](/providers/unosend) | Unique | Explicit | No | No | Yes | 75 | | [Lettermint](/providers/lettermint) | Unique | Both | Yes | Yes | Yes | 20 | | [Plunk](/providers/plunk) | Unique | Explicit | Yes | No | No | 0 | | [Helo](/providers/helo) | Unique | Both | Yes | Yes | No | 5 | | [JetEmail](/providers/jetemail) | Unique | ProviderSelected | No | No | Yes | 0 | | [Lettr](/providers/lettr) | Unique | Explicit | No | Yes | No | 1 | | [SparkPost](/providers/sparkpost) | Unique | Explicit | Yes | Yes | Yes | 0 | | [Primitive](/providers/primitive) | Unique | Both | Yes | No | No | 0 | | [Mailtrap](/providers/mailtrap) | Unique | Both | Yes | Yes | No | 1 | | [Mailchimp Transactional](/providers/mailchimp) | Unique | Explicit | Yes | Yes | Yes | 0 | | [Scaleway](/providers/scaleway) | Unique | Explicit | No | No | No | 0 | | [ZeptoMail](/providers/zeptomail) | Unique | Explicit | Yes | No | No | 0 | | [Cloudflare Email Sending](/providers/cloudflare) | Unique | Explicit | Yes | No | No | 0 | | [Amazon SES](/providers/ses) | Unique | Explicit | Yes | No | No | 50 | | [Microsoft Graph](/providers/graph) | Unique | Explicit | Yes | No | No | 0 | | [SMTP](/providers/smtp) | Repeated | Explicit | Yes | No | No | 0 | | [Brevo](/providers/brevo) | Unique | ProviderSelected | No | No | Yes | 0 | | [Sequenzy](/providers/sequenzy) | None | Both | No | No | No | 0 | | [SendHeron](/providers/sendheron) | Unique | Explicit | No | No | Yes | 0 | ## Read the table - `Unique` headers require distinct names ignoring case. `Repeated` preserves repeated names. A provider can additionally restrict which names are allowed. - `Explicit` attachment MIME selection requires a concrete content type. `ProviderSelected` delegates selection. `Both` supports either choice, never silent conversion between them. - A supported schedule still has provider-specific precision and time-window checks. Inline attachments can have additional CID or filename restrictions. - Tag counts do not describe allowed label syntax. Metadata support does not imply that nested application objects can be sent. ## Account and mode differences Lettr's base profile is Immediate; Scheduled mode enables scheduling and removes key support. Mailchimp's base profile disables tags; its explicit known-tag statistics configuration can enable a bounded subset. SendHeron's Disabled attachment mode removes attachment support and changes replay guarantees. Use the acquired `email.capabilities` and `email.personalization` for the configured service. The table does not cover Loops or Iterable native jobs, and field support alone never grants [idempotent replay](/concepts/idempotency). --- # Microsoft Graph Source: https://email-sdk.corvencore.com/providers/graph Import `graphEmailLayer` from `@corven/email-sdk/graph` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { graphEmailLayer } from "@corven/email-sdk/graph"; import { emailHttpLayer } from "./sending.js"; export const configuredGraphAccount = (accessToken: Redacted.Redacted) => graphEmailLayer({ accessToken, mailboxId: "00000000-0000-4000-8000-000000000001", mailboxAddress: "sender@example.test", senderIdentity: "OwnerOrSendAs", mailFlow: "ProviderUnmodified", sentItems: "Save", }).pipe(Layer.provide(emailHttpLayer)); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - The host acquires and refreshes accessToken. This adapter does not implement OAuth client credentials, managed identity discovery or token refresh. - Provide the mailbox UUID and address, attest OwnerOrSendAs permission and unmodified mail flow, and choose Save or DoNotSave for sent items. - Graph represents one body format; incompatible dual-body intent is rejected instead of dropping a body. Common metadata, tags and scheduling are unsupported. - An accepted `sendMail` response can omit the message ID. Store the receipt without adding an ID. ## Idempotency Idempotency keys are not supported. The SDK does not retry `sendMail` requests with unknown submission outcomes. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # Helo Source: https://email-sdk.corvencore.com/providers/helo Import `heloEmailLayer` from `@corven/email-sdk/helo` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { heloEmailLayer } from "@corven/email-sdk/helo"; import { emailHttpLayer } from "./sending.js"; export const configuredHeloAccount = (apiKey: Redacted.Redacted) => heloEmailLayer({ apiKey, channelMode: "Live" }).pipe(Layer.provide(emailHttpLayer)); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - Choose channelMode Live or Sandbox, and optionally supply a UUID channelId. The setting describes the selected channel; it does not create one. - Custom headers cannot override Helo controls. Recipient addresses must be distinct so suppression evidence can be assigned correctly. - Sandbox responses become Simulated. Live suppression evidence can produce partial or fully suppressed receipts. - Scheduling is unsupported; metadata, inline attachments and bounded tags are supported. ## Idempotency Native idempotency applies to the configured account and channel for one hour. Keys can contain up to 36 characters. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # Iterable triggered campaigns Source: https://email-sdk.corvencore.com/providers/iterable Use `IterableCampaignEmail` from `@corven/email-sdk/iterable`. This native campaign service is separate from common literal-message sending and ordinary routing. ```ts import { Effect, Redacted } from "effect"; import { IterableCampaignEmail, iterableCampaignLayer } from "@corven/email-sdk/iterable"; export const fictionalIterableLayer = iterableCampaignLayer({ apiKey: Redacted.make("fictional-unused-key"), campaignAuthority: "AuthorizeProviderManagedCampaign", contactWrites: "AuthorizeProfileCreation", dataRetention: "AuthorizePersistedSendData", projectIdentity: "EmailBased", region: "US", tracking: "AuthorizeCampaignTracking", }); export const fictionalIterableNotification = Effect.gen( function* submitFictionalIterableCampaign() { return yield* (yield* IterableCampaignEmail).send({ campaignId: 123, dataFields: { count: 0, firstName: "Fictional" }, recipientEmail: "recipient@fictional.invalid", }); }, ); ``` Provide a safe HTTP Layer before execution. The host selects the US or EU account region and supplies the API key. ## Review required settings Configuration requires an email-based project, provider-managed campaign authority, permission for profile creation, persisted send data and campaign tracking. These options record the application's permissions for provider behavior. They do not modify remote settings. ## Build a campaign job Supply a positive campaign ID and lowercase `recipientEmail`. Optional `dataFields` support bounded scalars and scalar arrays; metadata is a separate bounded record. Optional `sendAt` uses a representable whole-second UTC instant and remains subject to native dispatch validation. `validate` checks and owns the job without contacting the account. `sendAll` accepts native `{ job, options? }` envelopes and retains input order. The service does not implement the common personalization interface or turn campaign-managed content into a local template. ## Preserve replay limits Idempotency keys are not supported. The SDK does not retry requests with unknown submission outcomes. An accepted receipt can omit the message ID. It does not confirm delivery. Use [native defaults](/composition/native-defaults) for shared data fields, and [native instrumentation](/composition/native-instrumentation) for operation facts or authorized capture. --- # JetEmail Source: https://email-sdk.corvencore.com/providers/jetemail Import `jetemailEmailLayer` from `@corven/email-sdk/jetemail` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { jetemailEmailLayer } from "@corven/email-sdk/jetemail"; import { emailHttpLayer } from "./sending.js"; export const configuredJetemailAccount = (apiKey: Redacted.Redacted) => jetemailEmailLayer({ apiKey, automaticBcc: "DomainDisabled", tracking: "DomainDisabled", deliveryRegion: "ProviderSelected", }).pipe(Layer.provide(emailHttpLayer)); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - Use a transactional_ submission token. Attest that domain automatic BCC and tracking are disabled before supplying these options. - The sender requires an explicit display name; to recipients use plain mailboxes. Scheduled instants require whole seconds. - Attachments use ProviderSelected MIME selection. Inline attachments, common metadata and tags are unsupported. - deliveryRegion may be ProviderSelected or Europe; it does not establish cross-account deduplication. ## Idempotency The adapter forwards keys of up to 256 characters. Its idempotency declaration is `KeyForwarding`. The SDK does not retry requests with unknown submission outcomes. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # Lettermint Source: https://email-sdk.corvencore.com/providers/lettermint Import `lettermintEmailLayer` from `@corven/email-sdk/lettermint` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { lettermintEmailLayer } from "@corven/email-sdk/lettermint"; import { emailHttpLayer } from "./sending.js"; export const configuredLettermintAccount = (apiToken: Redacted.Redacted) => lettermintEmailLayer({ apiToken, route: "transactional" }).pipe(Layer.provide(emailHttpLayer)); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - Use apiToken and, optionally, a route slug. A route slug selects provider configuration; it is not an SDK fallback route. - Both explicit and provider-selected attachment MIME types are supported. Custom headers, metadata, scheduling and bounded ASCII tags are available. - Sandbox acknowledgment is reported as Simulated. A sandbox delivered label is not live delivery evidence. ## Idempotency Native idempotency applies to the configured account for 24 hours. Keys can contain up to 255 characters. The SDK stops retries after a payload conflict. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # Lettr Source: https://email-sdk.corvencore.com/providers/lettr Import `lettrEmailLayer` from `@corven/email-sdk/lettr` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { lettrEmailLayer } from "@corven/email-sdk/lettr"; import { emailHttpLayer } from "./sending.js"; export const configuredLettrAccount = (apiKey: Redacted.Redacted) => lettrEmailLayer({ apiKey, submissionMode: "Immediate" }).pipe(Layer.provide(emailHttpLayer)); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - Use a live lttr_ credential. Sandbox recipient rewriting is not supported. - Immediate and Scheduled are different configured modes. Scheduled mode enables scheduling and requires the scheduled acknowledgment shape. - Attachments require explicit MIME types. Inline attachments are unsupported; one tag and common metadata are available. - Acceptance/rejection counts do not identify which recipients belong to each group. Preserve count-only receipts. ## Idempotency Immediate mode declares native 24-hour replay using keys up to 255 characters containing only letters, digits, dots, underscores or hyphens. Scheduled mode declares no send key support. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # Loops transactional templates Source: https://email-sdk.corvencore.com/providers/loops Use `LoopsTransactionalEmail` from `@corven/email-sdk/loops`. This is a native published-template service, not a literal-message `Email` Layer or ordinary routing target. ```ts import type { Redacted } from "effect"; import { Effect, Layer } from "effect"; import { FetchHttpClient } from "effect/http"; import { LoopsTransactionalEmail, loopsTransactionalLayer } from "@corven/email-sdk/loops"; export const syntheticLoopsNotification = Effect.gen(function* submitNativeLoopsTemplate() { const loops = yield* LoopsTransactionalEmail; return yield* loops.send( { dataVariables: { invoiceNumber: "fictional-invoice", total: 0 }, email: "recipient@fictional.invalid", transactionalId: "fictional_published_template", }, { idempotencyKey: "fictional-invoice-notification" }, ); }); export const syntheticLoopsNotificationBatch = Effect.gen(function* submitNativeLoopsBatch() { const loops = yield* LoopsTransactionalEmail; return yield* loops.sendAll( [ { job: { dataVariables: { invoiceNumber: "fictional-first", total: 0 }, email: "first@fictional.invalid", transactionalId: "fictional_published_template", }, options: { idempotencyKey: "fictional-first-notification" }, }, { job: { dataVariables: { invoiceNumber: "fictional-second", total: 2 }, email: "second@fictional.invalid", transactionalId: "fictional_published_template", }, options: { idempotencyKey: "fictional-second-notification" }, }, ], { concurrency: 2 }, ); }); export const loopsNotificationLayer = (apiKey: Redacted.Redacted) => loopsTransactionalLayer({ apiKey, attachments: "Disabled", templateAuthority: "ProviderManaged", }).pipe( Layer.provide( FetchHttpClient.layer.pipe( Layer.provide(Layer.succeed(FetchHttpClient.RequestInit, { redirect: "manual" })), ), ), ); ``` ## Configure the template Supply `apiKey`, `templateAuthority: "ProviderManaged"` and an attachment policy of Disabled or AccountEnabled. These options authorize provider-owned content and describe known account prerequisites; the SDK does not publish or inspect a template. ## Build a job `send` takes `transactionalId`, one plain `email` recipient and optional `dataVariables`/attachments. Variables support bounded strings, finite numbers and bounded nonempty arrays of flat records. Arbitrary nested application JSON and booleans are not interchangeable with this input contract. Attachments require explicit MIME types, no inline CID placement and account enablement. Common sender/subject/body fields are not a native template job. `validate` returns an owned job without sending. `sendAll` takes `{ job, options? }` envelopes, returning ordered per-item Results with bounded concurrency. Defects and interruption still propagate. ## Handle acknowledgment and keys An accepted Loops receipt can omit the provider message ID. Store the receipt without adding an ID. The service forwards keys of up to 100 characters and declares `KeyForwarding`. The SDK does not retry requests with unknown submission outcomes. For shared variables or instrumentation, use [native defaults](/composition/native-defaults) and [native observations/capture](/composition/native-instrumentation). --- # Mailchimp Transactional Source: https://email-sdk.corvencore.com/providers/mailchimp Import `mailchimpEmailLayer` from `@corven/email-sdk/mailchimp` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { mailchimpEmailLayer } from "@corven/email-sdk/mailchimp"; import { emailHttpLayer } from "./sending.js"; export const configuredMailchimpAccount = (apiKey: Redacted.Redacted) => mailchimpEmailLayer({ apiKey, automaticFooter: "AccountDisabled", keyMode: "Live", messageRules: "AccountDisabled", }).pipe(Layer.provide(emailHttpLayer)); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - Use a live key and attest that account message rules and automatic footer insertion are disabled. - Per-recipient acknowledgments retain accepted and rejected evidence. Queue status does not prove delivery. - Tags are disabled by default. The optional tagStatistics configuration authorizes only a declared set of known account tag labels. - Explicit MIME types, inline attachments, metadata and scheduling are supported subject to provider validation. ## Idempotency The adapter declares no send idempotency support. A recipient message ID or queued result does not authorize another submission. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # MailerSend Source: https://email-sdk.corvencore.com/providers/mailersend Import `mailersendEmailLayer` from `@corven/email-sdk/mailersend` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { mailersendEmailLayer } from "@corven/email-sdk/mailersend"; import { emailHttpLayer } from "./sending.js"; export const configuredMailersendAccount = (apiKey: Redacted.Redacted) => mailersendEmailLayer({ apiKey }).pipe(Layer.provide(emailHttpLayer)); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - Attachments delegate MIME selection to the provider: use ProviderSelected rather than an explicit content type. - Use one reply-to mailbox. CC and BCC each have provider limits, and recipient names cannot contain commas or semicolons. - Suppression warnings can produce partial or fully suppressed receipts. Preserve those partitions rather than treating them as retryable failures. ## Idempotency Idempotency keys are not supported. The SDK does not retry requests with unknown outcomes or use a fallback account after a suppression result. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # Mailgun Source: https://email-sdk.corvencore.com/providers/mailgun Import `mailgunEmailLayer` from `@corven/email-sdk/mailgun` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { mailgunEmailLayer } from "@corven/email-sdk/mailgun"; import { emailHttpLayer } from "./sending.js"; export const configuredMailgunAccount = (apiKey: Redacted.Redacted) => mailgunEmailLayer({ apiKey, domain: "mail.example.test", region: "UnitedStates" }).pipe( Layer.provide(emailHttpLayer), ); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - The domain is a lowercase ASCII DNS name, not an API URL or account credential. Choose UnitedStates or Europe for the matching account. - Custom headers retain repeated names. Metadata maps to v: fields and tags to o:tag; control headers cannot override SDK submission controls. - Inline content IDs must equal the preserved filename. Scheduling requires whole seconds and a representable RFC-2822 year. - mailgunPersonalizedEmailLayer is a separate opt-in native audience mode. ## Idempotency Idempotency keys are not supported. The SDK does not retry requests with unknown submission outcomes. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # MailPace Source: https://email-sdk.corvencore.com/providers/mailpace Import `mailpaceEmailLayer` from `@corven/email-sdk/mailpace` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { mailpaceEmailLayer } from "@corven/email-sdk/mailpace"; import { emailHttpLayer } from "./sending.js"; export const configuredMailpaceAccount = (serverToken: Redacted.Redacted) => mailpaceEmailLayer({ serverToken }).pipe(Layer.provide(emailHttpLayer)); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - Use serverToken for this entrypoint, not the apiKey option used by other providers. - Only In-Reply-To, References and List-Unsubscribe custom headers are accepted. Use one plain reply-to address. - Attachments require explicit MIME types and a supported filename extension. The adapter accepts tags but not common metadata or scheduling. - The pending acknowledgment ID is a correlation handle, not delivery evidence. ## Idempotency Idempotency keys are not supported. The SDK does not retry requests with unknown submission outcomes. Reconcile the result in your application before sending again. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # Mailtrap Source: https://email-sdk.corvencore.com/providers/mailtrap Import `mailtrapEmailLayer` from `@corven/email-sdk/mailtrap` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { mailtrapEmailLayer } from "@corven/email-sdk/mailtrap"; import { emailHttpLayer } from "./sending.js"; export const configuredMailtrapAccount = (apiKey: Redacted.Redacted) => mailtrapEmailLayer({ apiKey, automaticBcc: "DomainDisabled", automaticFooter: "DomainDisabled", tracking: "DomainDisabled", }).pipe(Layer.provide(emailHttpLayer)); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - This entrypoint uses Email Sending, not an implicit local inbox or sandbox transport. - Attest that automatic BCC, footer insertion and tracking are disabled for the sending domain. The SDK does not inspect those settings. - Attachments support both MIME modes and inline placement. Metadata and one category tag are supported; scheduling is not. - Keep every ID in AcceptedWithIds. ID order does not map IDs to recipients. ## Idempotency This adapter declares no send key support. Multiple message IDs are correlation evidence, not replay permission. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # Plunk Source: https://email-sdk.corvencore.com/providers/plunk Import `plunkEmailLayer` from `@corven/email-sdk/plunk` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { plunkEmailLayer } from "@corven/email-sdk/plunk"; import { emailHttpLayer } from "./sending.js"; export const configuredPlunkAccount = (apiKey: Redacted.Redacted) => plunkEmailLayer({ apiKey, contactWrites: "AllowUpsert", tracking: "ProjectDisabled" }).pipe( Layer.provide(emailHttpLayer), ); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - Use a secret sk_ submission key. Public tracking keys are not accepted. - contactWrites permits provider contact upserts; tracking attests that project tracking is disabled. Neither option changes remote settings. - Plunk requires HTML and generates its own plain-text alternative. Supplying explicit text cannot preserve that intent and is rejected. - Use one plain reply-to mailbox. Tags, common metadata and scheduling are unsupported. ## Idempotency The adapter supports native idempotency within the configured account for 24 hours. Keys can contain up to 255 characters. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # Postmark Source: https://email-sdk.corvencore.com/providers/postmark Import `postmarkEmailLayer` from `@corven/email-sdk/postmark` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { postmarkEmailLayer } from "@corven/email-sdk/postmark"; import { emailHttpLayer } from "./sending.js"; export const configuredPostmarkAccount = (serverToken: Redacted.Redacted) => postmarkEmailLayer({ serverToken, messageStream: "outbound" }).pipe( Layer.provide(emailHttpLayer), ); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - Use a server submission token. The validation-only POSTMARK_API_TEST token is rejected at Layer acquisition. - The default message stream is outbound. Only one tag is supported; tag names cannot contain a colon. - Attachments require explicit MIME types. Scheduling is unsupported. ## Idempotency This integration declares no send idempotency support. Supplying an idempotency key fails locally; an unknown result cannot be replayed or moved to another account. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # Primitive Source: https://email-sdk.corvencore.com/providers/primitive Import `primitiveEmailLayer` from `@corven/email-sdk/primitive` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { primitiveEmailLayer } from "@corven/email-sdk/primitive"; import { emailHttpLayer } from "./sending.js"; export const configuredPrimitiveAccount = (apiKey: Redacted.Redacted) => primitiveEmailLayer({ apiKey, keylessSends: "Reject", tracking: "ProviderDisabled" }).pipe( Layer.provide(emailHttpLayer), ); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - Use a prim_ submission key; prim_oat_ credentials are excluded by this adapter. - Choose Reject to require caller keys, or explicitly AllowContentDeduplication for keyless sends. The latter permits provider content deduplication. The SDK still does not retry unknown submissions. - Only Auto-Submitted, X-Auto-Response-Suppress and Precedence custom headers are accepted. Metadata, tags and scheduling are unsupported. - Acknowledgments must describe a fresh submitted_to_agent operation with consistent recipient coverage. ## Idempotency The adapter forwards keys of up to 255 characters. Its idempotency declaration is `KeyForwarding`. The SDK does not retry requests with unknown submission outcomes. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # Resend Source: https://email-sdk.corvencore.com/providers/resend Import `resendEmailLayer` from `@corven/email-sdk/resend` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { resendEmailLayer } from "@corven/email-sdk/resend"; import { emailHttpLayer } from "./sending.js"; export const configuredResendAccount = (apiKey: Redacted.Redacted) => resendEmailLayer({ apiKey }).pipe(Layer.provide(emailHttpLayer)); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - Resend accepts unique custom headers, inline attachments and tags, but not common message metadata. - Use a future schedule within 30 days. A parsed timestamp still has to pass the provider check at dispatch. ## Idempotency Native idempotency applies to the configured account and unchanged request for 24 hours. Use the same key when retrying the operation. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # Scaleway Source: https://email-sdk.corvencore.com/providers/scaleway Import `scalewayEmailLayer` from `@corven/email-sdk/scaleway` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { scalewayEmailLayer } from "@corven/email-sdk/scaleway"; import { emailHttpLayer } from "./sending.js"; export const configuredScalewayAccount = (secretKey: Redacted.Redacted) => scalewayEmailLayer({ secretKey, projectId: "00000000-0000-4000-8000-000000000001", tracking: "ProviderDisabled", }).pipe(Layer.provide(emailHttpLayer)); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - Use a UUID projectId and a Redacted secretKey. tracking attests that provider tracking is disabled. - Attachments require explicit supported MIME types; inline placement, common metadata, tags and scheduling are unsupported. - Content and mailbox intent pass additional provider rules. Use the acquired service for final local validation. - Receipts preserve recipient outcomes and provider correlation instead of collapsing them to one sent flag. ## Idempotency Idempotency keys are not supported. The SDK does not retry requests with unknown submission outcomes. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # SendGrid Source: https://email-sdk.corvencore.com/providers/sendgrid Import `sendgridEmailLayer` from `@corven/email-sdk/sendgrid` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { sendgridEmailLayer } from "@corven/email-sdk/sendgrid"; import { emailHttpLayer } from "./sending.js"; export const configuredSendgridAccount = (apiKey: Redacted.Redacted) => sendgridEmailLayer({ apiKey, region: "Global" }).pipe(Layer.provide(emailHttpLayer)); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - Choose Global or Europe explicitly when account placement matters. Region selection does not create a new replay guarantee. - Sender display names must be ASCII. Reserved X-SG-ID and X-SG-EID headers cannot be overridden. - Schedules require a future whole UTC second within 72 hours. Metadata becomes custom arguments and tags become categories. - For grouped audiences, use sendgridPersonalizedEmailLayer deliberately; see the personalization guide. ## Idempotency Idempotency keys are not supported. The SDK does not retry requests with unknown submission outcomes. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # SendHeron Source: https://email-sdk.corvencore.com/providers/sendheron Import `sendheronEmailLayer` from `@corven/email-sdk/sendheron` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { sendheronEmailLayer } from "@corven/email-sdk/sendheron"; import { emailHttpLayer } from "./sending.js"; export const configuredSendheronAccount = (apiKey: Redacted.Redacted) => sendheronEmailLayer({ apiKey, attachments: "Disabled", submissionMode: "TransactionalOnly", tracking: "AuthorizeProviderTracking", }).pipe(Layer.provide(emailHttpLayer)); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - Authorize transactional provider tracking, and choose attachments Disabled when you need the native replay profile. - Intent is constrained to HTML, a named sender, one plain to recipient and one plain reply-to address; CC and BCC are unsupported. - Scheduled messages cannot include attachment bytes. Enabled attachments require explicit supported MIME types. - An acknowledged suppression is terminal; do not work around it by choosing another provider. ## Idempotency With attachments Disabled, native replay has a 24-hour retention window and keys up to 255 characters. With attachments enabled or unspecified, keys are only forwarded; unknown outcomes are not automatically replayable. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # Sequenzy Source: https://email-sdk.corvencore.com/providers/sequenzy Import `sequenzyEmailLayer` from `@corven/email-sdk/sequenzy` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { sequenzyEmailLayer } from "@corven/email-sdk/sequenzy"; import { emailHttpLayer } from "./sending.js"; export const configuredSequenzyAccount = (apiKey: Redacted.Redacted) => sequenzyEmailLayer({ apiKey, automaticUtm: "AccountDisabled", contactWrites: "AuthorizeProviderManagedProfiles", contentAuthority: "AuthorizeProviderRenderingAndRetention", replyTracking: "AccountDisabled", senderIdentity: "ExistingVerifiedProfile", submissionMode: "TransactionalOnly", }).pipe(Layer.provide(emailHttpLayer)); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - Configure an existing verified sender profile and explicitly authorize provider-managed profiles, rendering and retention. Automatic UTM and reply tracking must be disabled remotely. - The sender needs a display name, recipients must be plain and distinct across groups, and exactly one reply-to identity is required. - Only HTML is representable: explicit plain text, custom headers, common metadata, tags and scheduling are rejected. - Both attachment MIME modes are supported, without inline placement. ## Idempotency Native idempotency applies to the configured account for 14 days. Keys can contain up to 255 characters. The SDK stops retries after an idempotency conflict. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # Amazon SES Source: https://email-sdk.corvencore.com/providers/ses Import `sesEmailLayer` from `@corven/email-sdk/ses` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import { sesEmailLayer } from "@corven/email-sdk/ses"; import type { SesEmailOptions } from "@corven/email-sdk/ses"; import { emailHttpLayer } from "./sending.js"; export const configuredSesAccount = ( credentials: Pick, ) => sesEmailLayer({ ...credentials, mailFlow: "ProviderUnmodified", region: "us-east-1" }).pipe( Layer.provide(emailHttpLayer), ); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - The SDK signs requests itself using supplied accessKeyId, secretAccessKey and optional sessionToken. It does not use an ambient AWS credential chain. - Supported regions are us-east-1, us-west-2 and eu-west-1. mailFlow attests that provider behavior preserves the message data. - Use explicit MIME types and supported attachment extensions. Common metadata and scheduling are unsupported; bounded ASCII tags are available. ## Idempotency Idempotency keys are not supported. The SDK does not retry requests with unknown submission outcomes. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # SMTP Source: https://email-sdk.corvencore.com/providers/smtp Import `smtpEmailLayer` from `@corven/email-sdk/smtp` to provide the common `Email` service for this account. ## Configure the account ```ts import type { Redacted } from "effect"; import { smtpEmailLayer } from "@corven/email-sdk/smtp"; export const configuredSmtpAccount = (username: Redacted.Redacted, password: Redacted.Redacted) => smtpEmailLayer({ authentication: { _tag: "Plain", password, username }, helloName: "app.example.test", host: "smtp.example.test", mailFlow: "ProviderUnmodified", port: 465, security: "ImplicitTls", }); ``` SMTP manages its own explicit socket/TLS transport; it does not need an HTTP client. ## Message behavior - Configure an explicit host, port, helloName and ImplicitTls or StartTls security mode. Plain and Login authentication require Redacted credentials; None is also explicit. - Connecting to an IP requires an explicit certificate serverName. The adapter does not support an unencrypted SMTP mode. - Attachments use explicit MIME types; inline CIDs must be qualified. Repeated custom headers are supported, but metadata, tags and provider scheduling are not. - RCPT and DATA evidence can produce partial recipient outcomes. A final acknowledgment is not inbox delivery, and a lost DATA acknowledgment can leave the outcome unknown. ## Idempotency SMTP has no native send key support in this SDK. Reopening a connection cannot prove that a previous transaction was not accepted. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # SparkPost Source: https://email-sdk.corvencore.com/providers/sparkpost Import `sparkpostEmailLayer` from `@corven/email-sdk/sparkpost` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { sparkpostEmailLayer } from "@corven/email-sdk/sparkpost"; import { emailHttpLayer } from "./sending.js"; export const configuredSparkpostAccount = (apiKey: Redacted.Redacted) => sparkpostEmailLayer({ apiKey, region: "UnitedStates" }).pipe(Layer.provide(emailHttpLayer)); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - Choose UnitedStates or Europe for the matching account. Messages support explicit MIME types, inline attachments, unique headers, metadata and scheduling. - Common tags are unsupported by this integration. Do not replace them with substitution data to obtain a successful send. - Recipient acceptance counts remain count-only evidence; the transmission ID does not establish recipient identity. ## Idempotency Native idempotency applies for 24 hours. Keys can contain up to 255 letters, digits, dots, underscores or hyphens. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # Unosend Source: https://email-sdk.corvencore.com/providers/unosend Import `unosendEmailLayer` from `@corven/email-sdk/unosend` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { unosendEmailLayer } from "@corven/email-sdk/unosend"; import { emailHttpLayer } from "./sending.js"; export const configuredUnosendAccount = (apiKey: Redacted.Redacted) => unosendEmailLayer({ apiKey }).pipe(Layer.provide(emailHttpLayer)); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - Use explicit MIME types from the supported set, including PDF, PNG, JPEG, CSV, plain text, ZIP, JSON and HTML. - Inline attachments and common metadata are unsupported. Use at most one reply-to address. - A successful response must carry a consistent success marker and data.id; an unrecognized response remains unknown. ## Idempotency Idempotency keys are not supported. The SDK does not retry requests with unknown submission outcomes. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # Validation and verification boundaries Source: https://email-sdk.corvencore.com/providers/verification The checks below verify different properties. Use the check that matches the property you need to verify. | Evidence | What it establishes | What remains unknown | | ---------------------------- | ------------------------------------------------------------------- | --------------------------------------------------- | | Common message decoding | Input shape and common refinements | Provider support and remote permission | | Provider-local preflight | Implemented provider constraints | Credentials, account settings and remote acceptance | | Synthetic service tests | Application behavior under controlled outcomes | Real protocol or provider behavior | | Local loopback wire tests | Request/response and transport behavior against a local fixture | Live authentication and delivery | | Installed-consumer checks | Shipped exports, declarations and selected runtime interoperability | Every bundler/runtime and live account | | Explicit live authentication | The scoped account/authentication operation checked | General sending permission and delivery | | Provider acceptance | Acknowledgment of a submission | Recipient receipt or inbox placement | ## Use offline checks first Start with [recording and failing services](/guides/testing). Validate the complete message against the selected provider. Compile public-import examples in your application's actual TypeScript and optional-framework versions. The CLI is offline. It has no live doctor or send command. If credentials are unavailable, authentication has not been checked. ## Keep live probes controlled Before a live check, specify the account, destination, credentials, permitted data, expected cost and cleanup. Use fictional addresses in offline tests. Check submission and delivery separately. A receipt or parsed webhook does not verify all provider behavior. --- # ZeptoMail Source: https://email-sdk.corvencore.com/providers/zeptomail Import `zeptomailEmailLayer` from `@corven/email-sdk/zeptomail` to provide the common `Email` service for this account. ## Configure the account ```ts import { Layer } from "effect"; import type { Redacted } from "effect"; import { zeptomailEmailLayer } from "@corven/email-sdk/zeptomail"; import { emailHttpLayer } from "./sending.js"; export const configuredZeptomailAccount = (apiKey: Redacted.Redacted) => zeptomailEmailLayer({ apiKey, agentMode: "Live" }).pipe(Layer.provide(emailHttpLayer)); ``` The example reuses the [explicit HTTP Layer](/sending#configure-transport-behavior). Acquire credentials in trusted host code, then provide this account Layer to your sending program. ## Message behavior - agentMode describes the selected mail agent. Live and Sandbox produce different receipt meanings; the SDK does not toggle the remote agent. - Explicit MIME types, inline attachments and unique headers are supported. Metadata, tags and scheduling are unsupported. - Provider attachment extension restrictions still apply after common attachment validation. ## Idempotency The adapter declares no send key support. A sandbox acknowledgment is Simulated, not evidence of live acceptance or delivery. Read [field support](/providers/field-support) before switching accounts, and [receipts and retries](/reliability) before adding recovery behavior. Configuration and local validation do not authenticate the account or prove delivery. --- # Quickstart Source: https://email-sdk.corvencore.com/quickstart Record a notification with the public `Email` service. This example uses an in-memory Layer, so you can run it without a provider account or HTTP client. ## Requirements This guide covers SDK **0.1.0** with Effect **4.0.3** on Node.js **>=24.18.0**. The package supports ESM imports and Node's synchronous CommonJS loading of the same ESM graph. It does not ship a separate CommonJS bundle; older Node versions, browsers and edge runtimes are outside this runtime contract. ## 1. Install the package The SDK is currently available from a local checkout rather than a package registry. From that checkout, build a package archive using the package manager pinned in `package.json`: ```sh pnpm install --frozen-lockfile pnpm build pnpm pack --out ./corven-email-sdk.tgz ``` In your application, install the archive and its matching Effect peer: ```sh pnpm add /absolute/path/to/corven-email-sdk.tgz effect@4.0.3 ``` ## 2. Record an email ```ts import { Effect } from "effect"; import { Email } from "@corven/email-sdk"; import { RecordedEmails, recordingEmailLayer } from "@corven/email-sdk/testing"; export const recordNotification = Effect.gen(function* recordNotification() { const email = yield* Email; const records = yield* RecordedEmails; const receipt = yield* email.send({ body: { text: "Your fictional order is ready." }, from: "sender@example.test", subject: "Order ready", to: "recipient@example.test", }); const messages = yield* records.messages; return { messages, receipt }; }).pipe(Effect.provide(recordingEmailLayer)); ``` The program acquires `Email` to submit the message and `RecordedEmails` to inspect the recording. Providing `recordingEmailLayer` connects both services to the same in-memory store. All addresses in these examples use the fictional `.test` domain. ## 3. Run and inspect the result In your application's entrypoint, run `Effect.runPromise(recordNotification)`. The result contains: - `receipt`: a `Recorded` receipt identifying the local recording. - `messages`: the messages recorded in that Layer acquisition. The recording Layer stores messages in memory without contacting a provider. Use it to test application behavior. It does not persist a queue or confirm delivery. Acquire the recorder and its query service together. A new recording Layer has a separate store. ## Next steps [Configure a provider](/sending) when you are ready to connect an account, or read [messages and attachments](/messages) to build richer content. --- # API reference Source: https://email-sdk.corvencore.com/reference Use installed declarations for exact generic errors and Effect service requirements. The references below explain public fields, methods and behavioral constraints without requiring imports from private implementation files. ## Common service and data - [Email service](/reference/email): preparation, validation, sending, batching and personalization. - [Message](/reference/message): addresses, content, headers, tags, metadata and owned input. - [Attachments](/reference/attachments): acquisition helpers, byte bounds, MIME selection and placement. - [Options and policy](/reference/options): keys, complete policy overrides, private context and concurrency. - [Receipts](/reference/receipts): exact tags, IDs, count-only outcomes and recipient partitions. - [Errors](/reference/errors): expected tags, safe fields and integration failure reasons. ## Providers and composition - [Sender contract](/reference/sender): low-level validation/submission and the HTTP provider builder. - [Routing](/reference/routing): named account bindings, plans and routed receipts. - [Extension callbacks](/reference/callbacks): acquisition, retry restrictions and observation contracts. - [Field support](/providers/field-support): declared base provider capabilities and configuration differences. - [Composition](/composition): defaults, transformations, observations, capture and aggregation. Import these common contracts from `@corven/email-sdk`. Account Layers, native jobs and optional integrations use the explicit subpaths described in their guides. Run `corven-email discover` for the installed export inventory. ## Offline CLI The [CLI reference](/reference/cli) covers help, installed entrypoint discovery, selected provider profiles, partial JSON Schema and local request validation. It never acquires credentials or sends email. ## Runtime and scope Check [installation](/getting-started/installation) for Node loading and optional peers, and [scope](/reference/scope) before assuming that a similarly named feature in another SDK is implemented here. Acceptance, authentication, delivery and installed typing are distinct evidence boundaries. --- # Attachment reference Source: https://email-sdk.corvencore.com/reference/attachments Import `EmailAttachmentSchema`, `EmailAttachmentInput`, `EmailAttachmentOptions` and the acquisition helpers from the root entrypoint. | Helper | Supplied content | Result | | -------------------------------- | -------------------------- | ---------------------------------------------------- | | `emailAttachmentFromBytes` | `Uint8Array` plus options | Effect of `EmailAttachment` or `EmailInvalidMessage` | | `emailAttachmentFromArrayBuffer` | `ArrayBuffer` plus options | Effect of `EmailAttachment` or `EmailInvalidMessage` | | `emailAttachmentFromBlob` | `Blob` plus options | Effect of `EmailAttachment` or `EmailInvalidMessage` | The application acquires files or URLs; helpers do not fetch or read paths. ## Fields and bounds `filename` is a bounded ASCII basename, not a path. `contentType` is a lowercase MIME type/subtype without parameters or the literal `ProviderSelected`. `contentBase64` is canonical padded standard Base64 without whitespace or URL-safe characters. Each attachment contains from one byte through one MiB of decoded content. A message permits at most ten attachments and four MiB of decoded attachment content. A provider can impose additional encoded message-size, extension or type limits. Optional `contentId` is an unprefixed bounded ASCII identifier. Explicit `disposition: "inline"` requires it; `disposition: "attachment"` forbids it. IDs must be distinct within a message. Provider-specific CID constraints can be stricter. ## Preserve MIME selection Do not infer MIME from filenames or `Blob.type`. Choose `ProviderSelected` only for a compatible provider and a deliberate delegation. A provider requiring explicit MIME must refuse that delegation, and vice versa. Interruption can stop awaiting a Blob read without physically cancelling it. Rendering an inline CID reference does not prove that the provider or every email client supports the placement. See [field support](/providers/field-support). --- # Extension callbacks Source: https://email-sdk.corvencore.com/reference/callbacks `EmailExtension` accepts a callback directly or an Effect that acquires it. Acquire callback dependencies in the Layer scope. Provide all service dependencies before invoking the callback. Acquiring a callback does not change the configured submission policy. ## Retry callbacks ```ts import type { Redacted } from "effect"; import type { EmailRetryObserver, EmailRetryRestriction } from "@corven/email-sdk"; import { resendEmailLayer } from "@corven/email-sdk/resend"; export const accountWithRetryCallbacks = ( apiKey: Redacted.Redacted, observer: EmailRetryObserver, restriction: EmailRetryRestriction, ) => resendEmailLayer({ apiKey }, undefined, observer, restriction); ``` Provide the HTTP Layer before running this account. The optional callback arguments come after provider configuration and delivery policy. | Callback | Input | Result | | ----------------------- | --------------------- | ------------------------------------------------------------ | | `EmailRetryRestriction` | `EmailRetryCandidate` | Effect of boolean; false vetoes an otherwise eligible retry | | `EmailRetryObserver` | `EmailRetryDecision` | Effect of void, awaited after an eligible retry is permitted | Candidates have RejectedRetryCandidate or UnknownRetryCandidate tags. Decisions have RejectedRetry or UnknownRetry tags. Both carry provider, current/next attempt, delay and safe reason. A restriction can reject an eligible retry. It cannot enable a retry that the provider or policy does not support. An observer receives the retry decision and cannot change it. ## Other callback boundaries - [Transforms](/composition/transformations) return replacement message input. - [Submission observers](/composition/observations) receive minimal starting/settled sender facts. - [Operation observers](/composition/operations) receive complete private prepared-operation outcomes. - [Native observers](/composition/native-instrumentation) describe Loops/Iterable jobs rather than common sender invocations. All are trusted host Effects. Failures and interruption cannot roll back acknowledgment, become an ordinary retryable send error or authorize fallback. Keep private values out of diagnostic callback destinations and use explicit capture when payload retention is required. --- # Offline CLI reference Source: https://email-sdk.corvencore.com/reference/cli The package installs `corven-email`. It performs offline inspection only: no credential acquisition, environment discovery, authentication, host approval, sending or telemetry exporter is added by these commands. | Command | Reference | | ---------------------------------- | --------------------------------------------------- | | `corven-email help` / `--help` | [Usage](/reference/cli/help) | | `corven-email discover` | [Installed entrypoints](/reference/cli/discover) | | `corven-email providers` | [Local provider profiles](/reference/cli/providers) | | `corven-email schema` | [Partial JSON Schema](/reference/cli/schema) | | `corven-email validate [provider]` | [Request validation](/reference/cli/validate) | From a checkout, run `pnpm build` and replace `corven-email` with `node dist/email-cli-main.js`. ## Input and output Validation reads one UTF-8 JSON envelope from piped stdin. The envelope has `message` and optional `options`. It is not a raw provider payload. Private input is never echoed in reports. Successful commands write their report to stdout. Expected failures write a safe JSON error to stderr. Exit status is 0 for success, 1 for a validation/environment failure and 2 for invalid command usage. ## Unsupported commands There is no `send`, `auth`, `doctor` or host-bound dry-run command. Do not copy commands from another SDK with a similar name. Local validity does not establish remote authentication, acceptance or delivery; [provider verification](/providers/verification) explains the distinction. --- # corven-email discover Source: https://email-sdk.corvencore.com/reference/cli/discover ```sh corven-email discover ``` The report contains the package name, version and sorted public entrypoints. It excludes the manifest convenience subpath from the entrypoint inventory and marks provider capabilities unchecked and submission not attempted. ## Use the installed inventory Use this command when an application cannot import a documented entrypoint or may be using a different installed version. Match imports to the package's actual exports rather than reaching into private `dist/` files. Discovery reads bounded installed distribution metadata. Invalid or unavailable metadata fails safely; it does not search credential files, environment variables or provider accounts. This is a packaging check, not local message preflight or an account health check. Use [providers](/reference/cli/providers) for implemented inspection profiles and [validate](/reference/cli/validate) for request checks. --- # corven-email help Source: https://email-sdk.corvencore.com/reference/cli/help ```sh corven-email help corven-email --help ``` Running `corven-email` without arguments also shows help. It describes installed discovery, selected provider preflight, partial Schema output and stdin request validation. Help does not load credentials or contact a provider. Unknown commands and unsupported argument shapes return usage failure with exit status 2 instead of falling through to a live operation. Use [discover](/reference/cli/discover) to inspect installed entrypoints or [validate](/reference/cli/validate) to check a JSON request. A help listing is not a statement that every provider account or remote feature is configured. --- # corven-email providers Source: https://email-sdk.corvencore.com/reference/cli/providers ```sh corven-email providers ``` The report identifies selected local profiles and their capability declarations. Current implemented profiles are Resend, Postmark, SendGrid, Mailgun, MailerSend, MailPace and Unosend. This is a narrower set than the package's [sending integrations](/providers). Absence from this command does not mean a provider Layer is missing, and presence does not prove any account has been authenticated. ## Apply a profile Pass a supported lowercase profile name to `corven-email validate`, such as `resend` or `postmark`. The validator checks actual local provider rules in addition to common input decoding. Profiles do not acquire credentials, HTTP clients or remote settings. Account-dependent modes and native Loops/Iterable jobs are not represented by this command. Use the configured service's `validate` method when its actual account or mode affects support. See [capabilities and preflight](/providers/capabilities) before interpreting a capability flag as sendability. --- # corven-email schema Source: https://email-sdk.corvencore.com/reference/cli/schema ```sh corven-email schema ``` The command writes a report containing a JSON Schema `document`, marked as a `PartialProjection`, with `corven-email validate` identified as the validation authority. ## Use it for structural tooling The projection can help an editor or local form discover request structure. It does not fully represent custom refinements, provider-specific rules, account modes or every encoded Effect value. Run [validate](/reference/cli/validate) on the complete envelope before treating input as locally valid. Then use the actual configured service for account-dependent checks. A generated Schema is not a credential check, a send lease or remote acceptance evidence. The command reads no private request body and performs no provider operation. Do not use a successful JSON Schema check to remove or weaken SDK validation. --- # corven-email validate Source: https://email-sdk.corvencore.com/reference/cli/validate ```sh printf '%s' '{"message":{"from":"sender@example.test","to":"recipient@example.test","subject":"Example","body":{"text":"Fictional content"}}}' | corven-email validate ``` For provider-local checks, use `corven-email validate resend` or another name listed by [providers](/reference/cli/providers). The envelope is `{ message, options? }`, not a raw provider payload. ## Decode the input Input must be one piped UTF-8 JSON request within the 32-MiB input bound. Ambiguous duplicate JSON fields, invalid encoding, invalid request shape and unsupported arguments are refused. Unknown message/option properties are not silently discarded. Options use the CLI's JSON-compatible Schema representation. Do not assume that arbitrary serialized JavaScript `Date` or Effect Duration objects are valid input; use the [Schema projection](/reference/cli/schema) for structural guidance and this command as the actual local validator. ## Interpret success A success report distinguishes common-only decoding from selected provider-local preflight. It marks authentication and delivery unchecked, and submission not attempted. A true local `valid` result is not a provider account check or permission to send. Private input is not echoed. Keep credentials and private messages out of command-line arguments; pipe or redirect an authorized local input source instead. Expected input failures return exit status 1 with a safe error reason. Unsupported command usage returns 2. --- # Email service reference Source: https://email-sdk.corvencore.com/reference/email Import `Email` and `IEmail` from `@corven/email-sdk`. Acquire the service inside an Effect and provide a configured Layer before execution. ## Methods | Method | Success | Expected failure | | ----------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------ | | `prepare(message, options?)` | `EmailPreparedOperation` | `EmailInvalidMessage`, `EmailInvalidOptions`, `EmailRejected` | | `validate(message, options?)` | `EmailMessage` | `EmailInvalidMessage`, `EmailInvalidOptions`, `EmailRejected` | | `send(message, options?)` | `EmailReceipt` | `EmailSendError` | | `sendAll(requests, options?)` | Readonly array of `Result` | `EmailInvalidOptions` for invalid batch options | | `sendPersonalized(input, options?)` | Readonly array of `Result` | `EmailInvalidMessage`, `EmailInvalidOptions` for invalid operation input | All methods return Effects. Per-item expected batch failures live inside Results; defects and interruption remain Effect causes. Layer acquisition errors belong to composition before these methods are available. ## Prepared operation `previews` is a statically nonempty readonly sequence of `EmailPreparedPreview` values. Each contains `message`, `options` and `routes` after effective preparation. `validate` is an Effect returning void; `send` is an Effect returning the receipt or `EmailSendError`. Preparation runs local checks without submitting. After review, execute the same prepared operation while its scope remains open. The operation is not a persistent approval record. Executing its send Effect again requires the same retry checks as any other send. ## Service properties `capabilities` describes the configured service's locally supported features. `personalization` is `Individual` or `NativeAudience`. Neither property authenticates a remote account. `IEmailRoute` extends the service with routed receipts containing `route`. Native Loops/Iterable services are separate interfaces, not implementations of this common literal-message contract. See [sending modes](/concepts/sending-modes) and [routing reference](/reference/routing). --- # Error reference Source: https://email-sdk.corvencore.com/reference/errors Errors use Schema-tagged classes. `EmailSendError` is the union of invalid message, invalid options and submission errors; `EmailSubmissionError` contains `EmailRejected` and `EmailDeliveryUnknown`. Configuration can fail before a service is acquired. ## Common failures | Error | Safe fields | Meaning | | ------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------- | | `EmailInvalidMessage` | `issues` | Common or provider-local message refusal | | `EmailInvalidOptions` | `issues` | Invalid keys, policies, routing or operation options | | `EmailConfigurationError` | `issues` | Invalid Layer configuration or composition | | `EmailRejected` | `provider`, `reason`, optional `status`, `providerCode`, `retryAfterMillis` | Definite rejection evidence | | `EmailDeliveryUnknown` | Same contextual fields and its own `reason` union | Submission outcome could not be established | Issue entries contain safe `path` and `problem` descriptions, not private input values or original parser/provider errors. ### Rejection reasons Reasons are `authentication`, `rate-limited`, `temporarily-unavailable`, `invalid-message`, `idempotency-conflict`, `unsupported-idempotency` and `rejected`. Not every definite rejection permits retry or fallback. Conflicts remain terminal; a later rejection cannot erase an earlier unknown attempt. ### Unknown reasons Reasons are `transport`, `timeout`, `invalid-response`, `server-error`, `unexpected-status` and `idempotency-in-progress`. Retain uncertainty. A retry requires provider-supported idempotency for the selected account, the same key and prepared message, and a policy and error reason that permit it. ## Integration failures | Error | Reasons or boundary | | -------------------------------- | --------------------------------------------------------------------- | | `EmailRenderError` | `invalid-template`, `render-failed`, `invalid-output` | | `EmailAgentApprovalError` | `declined`, `unavailable` | | `EmailAiToolError` | `defect`, `not-completed`; no assumption of non-submission | | `EmailWebhookVerificationError` | `invalid-request`, `timestamp-outside-tolerance`, `invalid-signature` | | `EmailWebhookNormalizationError` | `invalid-json`, `invalid-payload`, `payload-limit` | | Aggregate errors | An observation or capture sample could not be read or validated | Import integration errors from their corresponding public entrypoint. The executable CLI reports its own safe error reason and [exit code](/reference/cli), not a private thrown provider exception. ## Preserve causes Defects and interruption remain Effect causes. They cannot be converted into an ordinary settled batch failure or selected away from a compound cause to make replay seem eligible. Follow the [troubleshooting guide](/guides/troubleshooting) for application recovery. --- # Message reference Source: https://email-sdk.corvencore.com/reference/message `EmailMessageInput` is the encoded caller input. `parseEmailMessage` decodes it into an owned `EmailMessage` or fails with `EmailInvalidMessage`. It does not run provider preflight. | Field | Input and constraint | | ---------------------- | ------------------------------------------------------------------ | | `from` | One mailbox | | `to` | One mailbox or a statically nonempty recipient array | | `subject` | Nonempty string, at most 998 characters; no CR, LF or NUL | | `body` | Nonempty `text`, `html`, or both; each representation is bounded | | `cc`, `bcc`, `replyTo` | Optional nonempty recipient input | | `headers` | Optional nonempty array of custom `{ name, value }` entries | | `tags` | Optional nonempty array of distinct-name `{ name, value }` entries | | `metadata` | Optional bounded string-to-string record | | `attachments` | Optional nonempty array of canonical attachment inputs | | `sendAt` | Optional absolute scheduled instant | The common message permits at most 50 total `to`/`cc`/`bcc` recipients. Provider rules can be stricter, including one-recipient or distinct-recipient requirements. Unknown excess fields are refused rather than silently discarded. ## Mailboxes Use a conservative ASCII address, `{ address, name? }`, or one bounded named mailbox string. Internationalized local parts and arbitrary RFC list syntax are unsupported. `formatEmailMailbox` formats an already decoded mailbox; it does not establish provider support. ## Headers, tags and metadata Headers cannot replace routing, authentication or MIME control fields. Names are bounded ASCII tokens and values exclude controls. Providers can further restrict names and repeated-name behavior. Common tags require distinct names; provider tag count, alphabet and label formatting still apply. Common metadata permits bounded strings rather than nested JSON. Private `executionMetadata` belongs to [operation options](/reference/options), not this provider-facing field. ## Ownership and richer inputs Decoding owns nested message values so later caller mutation cannot change the prepared message and options. Read [attachments](/reference/attachments) for byte and MIME selection, and [scheduling](/guides/scheduling) for accepted instant forms and dispatch-time checks. The [construction example](/messages) demonstrates ordinary inputs without caller casts. --- # Send options and delivery policy Source: https://email-sdk.corvencore.com/reference/options `EmailSendOptionsInput` contains optional `idempotencyKey`, `policy` and `executionMetadata`. Options are separate from provider-facing message fields. ## Delivery policy ```ts import { Duration, Effect } from "effect"; import { Email } from "@corven/email-sdk"; export const submitWithSingleAttempt = Effect.gen(function* submitWithSingleAttempt() { const email = yield* Email; return yield* email.send( { body: { text: "Your notification is ready." }, from: "sender@example.test", subject: "Notification", to: "recipient@example.test", }, { policy: { deadline: Duration.seconds(10), initialDelay: Duration.millis(100), maxAttempts: 1, }, }, ); }); ``` A policy is a complete object, not a partial merge: | Field | Meaning | Constraint | | -------------- | ------------------------------------------- | ------------------------------- | | `deadline` | Total owned submission/retry budget | Positive finite Effect Duration | | `initialDelay` | Initial retry backoff | Positive finite Effect Duration | | `maxAttempts` | Total allowed attempts, including the first | Integer from 1 through 6 | The built-in default is a 30-second deadline, 100-millisecond initial delay and three attempts. Provider convenience Layers accept a policy; an ordinary operation's complete override replaces it. Retries use exponential jittered delay and supported provider delay hints, subject to evidence and authority limits. Setting `maxAttempts: 1` does not guarantee non-submission after timeout. A native replay policy with multiple attempts must fit inside the provider retention window. See [idempotency](/concepts/idempotency). ## Keys and execution context `idempotencyKey` must satisfy the common bounded alphabet and the provider's narrower requirements. Unsupported keys fail before transport. Keys do not transfer across accounts. `executionMetadata` is bounded private host context for preparation, approval and complete operation observation. It is not automatically provider metadata or model input. ## Batch and personalized options `sendAll` envelopes each contain `{ message, options? }`; shared batch options contain `concurrency`, bounded from 1 through 32. Personalization options may contain concurrency and execution context; keys belong to individual audience entries. A native job batch uses `{ job, options? }` instead of a literal-message envelope. Expected option decoding fails with `EmailInvalidOptions`. Defects and interruption are never ordinary invalid-option errors. --- # Receipt reference Source: https://email-sdk.corvencore.com/reference/receipts Narrow `EmailReceipt` with `_tag` or exhaustive Schema dispatch. Preserve the complete value; success is not a universal delivered status. | `_tag` | Principal fields | | ------------------------- | ---------------------------------------------------------------------- | | `Accepted` | `provider`, optional `messageId`, optional `submissionId` | | `AcceptedWithIds` | `provider`, nonempty `messageIds` | | `PartiallyAccepted` | `provider`, nonempty `accepted` and `suppressed`, optional `messageId` | | `PartiallyRejected` | `provider`, nonempty `accepted` and `rejected` | | `PartiallyAcceptedCounts` | `provider`, accepted/rejected `counts`, optional `messageId` | | `RecipientOutcomes` | `provider`, nonempty `outcomes`, optional `messageId` | | `RecipientsRejected` | `provider`, nonempty `rejected`, optional `messageId` | | `Suppressed` | `provider`, nonempty `suppressed` | | `Recorded` | Local recording `sequence` | | `Simulated` | `provider`, optional `messageId` | `EmailRecipientOutcome` has Accepted and Rejected cases with explicit recipient identity. An accepted outcome has a message ID; a rejected outcome may have one. Count-only evidence has no implied recipient mapping. ## Correlation and delivery Message IDs, arrays of IDs and native submission IDs identify different provider handles. Their presence or order does not prove delivery, duplicate prevention or independent outcomes for a grouped request. Native jobs can acknowledge without a message ID. `EmailRoutedReceipt` adds the actual `route` to the receipt family. Store that route in recovery state and use it when resuming the operation. ## Terminal outcomes Partial, suppressed, rejected-recipient, simulated and recorded receipts do not authorize fallback or another send. Synthetic recording is not live submission. For error recovery, use [receipts and retries](/reliability), not a conversion of these variants to boolean success/failure. --- # Routing reference Source: https://email-sdk.corvencore.com/reference/routing `emailRouterLayer(bindings, defaultPlan)` acquires named account Layers and exposes `EmailRouter`. A binding contains `name` and `layer`; names are bounded lowercase identifiers rather than private account labels. ## Route plan | Field | Meaning | | ----------- | ------------------------------------------------------------ | | `primary` | Required registered route name | | `fallbacks` | Optional nonempty ordered list of distinct registered routes | | `deadline` | Optional positive finite Effect Duration covering the chain | The primary and fallbacks must be distinct. `router.select(plan?)` returns an `IEmailRoute`; omit the argument to use the configured default plan. Routed sends return `EmailRoutedReceipt` values containing the actual route. ## Validation and authority Every selected candidate's effective message, key and policy is preflighted before transport. Unsupported backup intent is a local failure; fields cannot be deleted during fallback. Routed capabilities describe shared support, not cross-account native replay. Only eligible definite rejection advances the chain. Unknown outcomes, conflicts, terminal receipts, defects and interruption stop it. Native-audience candidates are refused rather than converted to individual execution. ## Instrumented accounts `observedEmailRouterLayer` and `capturedEmailRouterLayer` retain named account views for [aggregation](/composition/aggregation). They do not make samples atomic or persist route selection across process restarts. Use the [routed account example](/batching-routing#route-explicit-accounts) for complete public imports and transport composition. Your application owns durable selected-account recovery. --- # Scope and compatibility Source: https://email-sdk.corvencore.com/reference/scope Corven is an Effect-native email library with explicit provider Layers, local validation and typed submission evidence. It uses your existing provider accounts and runs inside your application. ## Supported features The [provider guides](/providers) describe ordinary literal-message Layers and the separate native Loops/Iterable services. The package also includes local recording/failing services, preparation/defaults/transformations, named routing, observation/capture views, React rendering, host-approved agent tools and selected webhook verification/projection. The [CLI](/reference/cli) is offline. [Machine-readable documentation](/agents) consists of static retrieval artifacts. Neither is a hosted email account, authentication endpoint or sending service. ## Application requirements Durable queues, background schedulers, provider account provisioning, DNS configuration, delivery reconciliation and persistent webhook replay storage belong to the host application. The SDK does not supply a first-party Convex component, Chat SDK integration, CRM or campaign builder. Graph tokens are host-acquired; there is no built-in OAuth discovery/refresh workflow. Cloudflare sending uses REST, not a Workers binding or inbound routing handler. These restrictions differ from capabilities advertised by some similarly named integrations in other libraries. ## Documentation comparisons This site includes [field support](/providers/field-support), decision-oriented [provider comparisons](/guides/provider-comparisons) and application React recipes. It does not claim a hosted preview service, community registry, universal plugin compatibility layer, provider pricing feed, DNS checker, blog history or migration compatibility with another SDK's releases. Use [runtime requirements](/getting-started/installation) for supported Node loading. A compiled example does not establish every runtime, bundler, remote account prerequisite or live delivery guarantee. --- # Sender and provider contract Source: https://email-sdk.corvencore.com/reference/sender `EmailSender` is a public Effect service. `emailLayer` turns it into the common higher-level workflow. Normal consumers use configured provider convenience Layers. ## IEmailSender | Member | Contract | | -------------------- | ---------------------------------------------------------------- | | `provider` | Static non-private provider identifier | | `capabilities` | Owned local feature declaration | | `idempotency` | None, KeyForwarding or Native declaration with applicable bounds | | `validate(message)` | Closed Effect of void or `EmailInvalidMessage`; no submission | | `submit(submission)` | Closed Effect of `EmailReceipt` or `EmailSubmissionError` | `EmailSubmission` contains an owned decoded message and optional branded key. Keep account identity stable across attempts. Returned receipts and error classification must follow actual provider evidence. ## HTTP provider builder `defineEmailHttpProvider` takes a configuration Schema, credential-free profile, static span label and `makeProtocol` acquisition Effect. It returns `profile`, `preflight`, `senderLayer` and `emailLayer`. See [the complete authoring example](/guides/authoring-provider). The builder validates decoded configuration on the Schema's type side and enforces capability checks before provider-specific local rules. Hooks must close their dependencies; acquire resources inside `makeProtocol` and preserve the owning scope. Use this builder only when local capabilities and idempotency do not depend on account configuration. More specialized adapters can implement `IEmailSender` directly. ## Author responsibilities Encode every supported field or reject it before transport. Preserve ambiguous outcomes and expected rejection distinctions. Do not add hidden mutation retries, redirects, sensitive diagnostics or runtime execution inside a Layer factory. A declared Native profile is a trusted author claim about the exact provider/account/operation, not a capability the SDK can discover from headers or IDs. Test protocol and public consumer behavior before claiming support; [verification boundaries](/providers/verification) still apply. --- # Receipts, errors and retries Source: https://email-sdk.corvencore.com/reliability A successful send returns an `EmailReceipt`. Inspect `_tag` to determine the submission result. An accepted receipt confirms that the provider acknowledged the request. It does not confirm that the recipient received the email. Store definite rejections and unknown outcomes as separate application states. Use the stored state when deciding whether to retry. ## Interpret receipts | Receipt `_tag` | Meaning | | ----------------------------------------- | ---------------------------------------------------------------------------------------------- | | `Accepted` / `AcceptedWithIds` | The provider acknowledged the request. Correlation IDs may be absent, singular or multiple. | | `PartiallyAccepted` / `PartiallyRejected` | The provider reported accepted and suppressed or rejected recipient partitions. Preserve both. | | `PartiallyAcceptedCounts` | The provider reported counts without identifying the recipients in each group. | | `RecipientOutcomes` | The receipt contains explicit per-recipient results. | | `Suppressed` | The provider reported suppression. Do not bypass it through another account. | | `RecipientsRejected` | The provider reported terminal rejection for the listed recipients. | | `Recorded` | An in-memory recorder captured the message without contacting a provider. | | `Simulated` | A provider sandbox acknowledged the request; this is not a live accepted submission. | A grouped request can include a `submissionId`. This identifies the request, not an individual message. Use explicit recipient results when available. Do not infer recipient results from counts or ID order. Message and submission IDs do not enable idempotent retries. ## Handle expected errors | Error | Response | | --------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | `EmailInvalidMessage` / `EmailInvalidOptions` | Correct the local input before trying again. | | `EmailConfigurationError` | Correct Layer configuration; this error can occur before a service is acquired. | | `EmailRejected` | Inspect the reason. Only eligible definite rejections permit retry or fallback; conflicts are terminal. | | `EmailDeliveryUnknown` | Persist the uncertainty. Do not report the message as unsent or switch accounts to resend it. | Handle these tagged errors through Effect's expected-error channel. Defects and interruption remain Effect causes; they are not ordinary send errors or settled batch results. SDK diagnostics provide safe categories and paths. Keep original causes, provider payloads and private message inputs out of logs. ## Configure retries Set a delivery policy on the provider Layer, or supply a complete policy override in send options. The policy sets attempt limits and deadlines. The SDK also checks whether the provider supports the requested retry. Replaying an unknown outcome requires all of the following: - Provider-supported idempotency for the selected account and operation. - The same idempotency key and unchanged prepared message. - A key within the provider's retention window and a policy that permits the retry. Store each key with its application event and creation time. Key forwarding alone does not enable retries after an unknown outcome. If an earlier attempt has an unknown outcome, retain that state after a later rejection. Do not retry because an observer failed. ## Recover from timeouts and process loss A timeout interrupts SDK-owned work but cannot recall a request the provider already accepted. Preserve uncertainty and reconcile through an application-owned process when the outcome cannot be established. The SDK does not provide a durable queue or exactly-once delivery. Your application must retain recovery state and any route selected across restarts. See [account routing](/batching-routing#route-explicit-accounts) before adding fallback; restarting from a different account can duplicate the same logical email. --- # Send through a provider Source: https://email-sdk.corvencore.com/sending A sending program depends on `Email`. Your application supplies a provider Layer to select the account and a transport Layer to make network requests. Keeping those dependencies separate lets you reuse the program with an offline recorder or another compatible provider. ## Configure Resend The following example provides `resendEmailLayer` with a `Redacted` API key and an explicit HTTP client: ```ts import { Effect, Layer } from "effect"; import type { Redacted } from "effect"; import { FetchHttpClient } from "effect/http"; import { Email } from "@corven/email-sdk"; import { resendEmailLayer } from "@corven/email-sdk/resend"; export const emailHttpLayer = FetchHttpClient.layer.pipe( Layer.provide(Layer.succeed(FetchHttpClient.RequestInit, { redirect: "manual" })), ); export const sendNotification = (apiKey: Redacted.Redacted) => Effect.gen(function* sendNotification() { const email = yield* Email; return yield* email.send( { body: { text: "Your fictional order is ready." }, from: "sender@example.test", subject: "Order ready", to: "recipient@example.test", }, { idempotencyKey: "orders/order-42/ready" }, ); }).pipe(Effect.provide(resendEmailLayer({ apiKey }).pipe(Layer.provide(emailHttpLayer)))); ``` Acquire `apiKey` in trusted application code before calling `sendNotification`. The SDK does not read environment variables, credential files or cloud identity endpoints. Keep credentials out of source code, logs, browser bundles and model input. ## Execute the program Calling `sendNotification(apiKey)` constructs an Effect. Run the Effect in your application's entrypoint to submit the provider request. Before running it: - Replace the fictional `.test` addresses with addresses your account is authorized to use. - Choose and persist an idempotency key for the logical event. The example key identifies one fictional order notification; unrelated notifications need different keys. - Handle the receipt and typed failures as described in [receipts, errors and retries](/reliability). ## Configure transport behavior The example sets `redirect: "manual"` to disable automatic redirects. Your HTTP client must also avoid hidden send retries and sensitive request or response logging. A retry underneath the SDK can repeat a request whose outcome is unknown, outside the SDK's submission policy. You can reuse a transport Layer across provider accounts. Idempotency remains specific to each account. ## Change providers For Postmark, import `postmarkEmailLayer` from `@corven/email-sdk/postmark` and supply its `serverToken`. The application still uses `Email`, but the provider must support every field and option you request. Postmark does not support idempotency keys in this SDK, so the keyed Resend example fails local validation. Select a provider that supports keys if your workflow requires them. Scheduling, attachments and recipient results also vary by provider. Check [providers and validation](/providers) before switching. Handle unsupported fields as validation errors; do not drop them automatically. Use the higher-level `Email` service for normal application workflows. `EmailSender` is the lower-level submission capability used to build provider integrations. --- # Email UI and template recipes Source: https://email-sdk.corvencore.com/ui The optional React entrypoint supplies a small themed component family and a renderer. Start with [components](/ui/components) and [themes](/ui/themes), then adapt a recipe to your application's authorized data. ## Account messages - [Verification code](/ui/verification-code): display a short-lived code and its expiry. - [Password reset](/ui/password-reset): link to an application-owned reset flow. - [Team invitation](/ui/team-invite): identify the inviter and destination team. ## Product notifications - [Welcome](/ui/welcome): offer one useful first action. - [Trial ending](/ui/trial-ending): state the deadline and link to plan settings. - [Usage alert](/ui/usage-alert): show measured usage and the relevant limit. ## Billing messages - [Payment receipt](/ui/receipt): describe an authoritative payment record. - [Invoice due](/ui/invoice-due): show an amount, due date and trusted review link. - [Refund confirmation](/ui/refund-confirmed): distinguish recorded refund state from settlement time. These examples use public SDK components. Copy and adapt them in your application; they are not SDK template exports. They return HTML and text bodies for [sending](/integrations/react). They do not implement authentication, billing or consent, and they do not send email. ## Review rendered messages Keep template code, links and business decisions trusted. Review plain text as well as HTML, and inspect the result in the email clients your audience uses. Compilation and rendering do not prove client compatibility, accessibility in every inbox or delivery. The SDK does not host previews or collect message content for analytics. --- # React email components Source: https://email-sdk.corvencore.com/ui/components `themedReactEmailComponents(themeOrPreset?)` returns a component family. Pass `"light"`, `"dark"` or a `ReactEmailTheme`; the default preset is light. | Component | Special props | Rendered structure | | ---------------- | ----------------------------------------------------- | ----------------------------------------------------------------- | | `EmailDocument` | `preview?`, `bodyStyle?`, HTML props including `lang` | HTML/head/body with theme styles and optional hidden preview text | | `EmailCard` | Table props including `style` | Presentation table containing the card content | | `EmailHeading` | `level?: 1 \| 2 \| 3` | The corresponding heading element; level defaults to 1 | | `EmailParagraph` | `tone?: "normal" \| "muted"` | Paragraph with normal or muted text styling | | `EmailAction` | Required `href`, `variant?: "solid" \| "outline"` | Styled anchor; variant defaults to solid | | `EmailDivider` | Horizontal-rule props | Themed separator | Components also accept the corresponding native React element props. Style overrides are trusted host input, not sanitized design tokens. An action renders a link, not an authenticated operation. ## Shared recipe layout The [template recipes](/ui) use this application helper to keep their structure consistent: ```ts import { createElement } from "react"; import { themedReactEmailComponents } from "@corven/email-sdk/react"; const { EmailAction, EmailCard, EmailDocument, EmailHeading, EmailParagraph } = themedReactEmailComponents("light"); interface TransactionalEmailLayoutInput { readonly heading: string; readonly paragraphs: readonly string[]; readonly preview: string; readonly action?: { readonly href: string; readonly label: string }; } export const transactionalEmailLayout = (input: TransactionalEmailLayoutInput) => createElement( EmailDocument, { lang: "en", preview: input.preview }, createElement( EmailCard, null, createElement(EmailHeading, null, input.heading), ...input.paragraphs.map((text, index) => createElement(EmailParagraph, { key: index }, text)), input.action === undefined ? null : createElement(EmailAction, { href: input.action.href }, input.action.label), ), ); ``` The helper is example code, not an SDK export. It creates trusted elements; `renderReactEmail` converts those elements into a body as shown in [React rendering](/integrations/react). Hidden preview content is omitted from the renderer's plain-text conversion. Review both representations before submission. No component validates link origins, authorizes recipients or guarantees email-client rendering. --- # Invoice due Source: https://email-sdk.corvencore.com/ui/invoice-due This application recipe uses the [shared transactional layout](/ui/components#shared-recipe-layout). It returns a rendering Effect containing HTML and plain text; it does not submit email or implement the business operation. ```ts import { renderReactEmail } from "@corven/email-sdk/react"; import { transactionalEmailLayout } from "./react-layout.js"; export const invoiceDueEmail = (input: { invoice: string; amount: string; dueAt: string; paymentUrl: string; }) => renderReactEmail( transactionalEmailLayout({ heading: "Invoice due", preview: "An invoice is ready for review.", paragraphs: [`Invoice ${input.invoice}: ${input.amount}.`, `Due: ${input.dueAt}.`], action: { href: input.paymentUrl, label: "Review invoice" }, }), ); ``` ## Supply authorized data Validate payment-link origins and the recipient relationship to the invoice. Retain invoice identity and the reminder event separately so retries do not create duplicate billing actions. Keep URLs and template inputs under host control. React text escaping is not URL validation or a consent check. Treat the rendered body as private message content. ## Send the rendered body Yield the rendering Effect in your application, then supply its body to `Email.send` with explicit sender and recipients. Apply ordinary [provider validation](/providers/capabilities), [submission policy](/reference/options) and any required approval. The [React integration](/integrations/react) shows this composition. These recipes are application examples, not additional template exports from the SDK. Rendering and compilation do not establish delivery or compatibility with every email client. --- # Password reset Source: https://email-sdk.corvencore.com/ui/password-reset This application recipe uses the [shared transactional layout](/ui/components#shared-recipe-layout). It returns a rendering Effect containing HTML and plain text; it does not submit email or implement the business operation. ```ts import { renderReactEmail } from "@corven/email-sdk/react"; import { transactionalEmailLayout } from "./react-layout.js"; export const passwordResetEmail = (input: { resetUrl: string; expiresIn: string }) => renderReactEmail( transactionalEmailLayout({ heading: "Reset your password", preview: "A password reset was requested for your account.", paragraphs: [ `Use the link below within ${input.expiresIn}.`, "If you did not request a reset, you can ignore this message.", ], action: { href: input.resetUrl, label: "Reset password" }, }), ); ``` ## Supply authorized data Generate a short-lived, single-use reset token in the host. Validate the destination origin, bind the request to the account and invalidate the token after use. Rendering does not implement authentication or token storage. Keep URLs and template inputs under host control. React text escaping is not URL validation or a consent check. Treat the rendered body as private message content. ## Send the rendered body Yield the rendering Effect in your application, then supply its body to `Email.send` with explicit sender and recipients. Apply ordinary [provider validation](/providers/capabilities), [submission policy](/reference/options) and any required approval. The [React integration](/integrations/react) shows this composition. These recipes are application examples, not additional template exports from the SDK. Rendering and compilation do not establish delivery or compatibility with every email client. --- # Payment receipt Source: https://email-sdk.corvencore.com/ui/receipt This application recipe uses the [shared transactional layout](/ui/components#shared-recipe-layout). It returns a rendering Effect containing HTML and plain text; it does not submit email or implement the business operation. ```ts import { renderReactEmail } from "@corven/email-sdk/react"; import { transactionalEmailLayout } from "./react-layout.js"; export const paymentReceiptEmail = (input: { reference: string; amount: string; detailsUrl: string; }) => renderReactEmail( transactionalEmailLayout({ heading: "Payment receipt", preview: "Your payment receipt is available.", paragraphs: [`Payment reference: ${input.reference}.`, `Amount: ${input.amount}.`], action: { href: input.detailsUrl, label: "View receipt" }, }), ); ``` ## Supply authorized data Render an amount and currency formatted by your billing system. Send only after authoritative payment state permits a receipt; provider acceptance cannot establish payment success. Keep URLs and template inputs under host control. React text escaping is not URL validation or a consent check. Treat the rendered body as private message content. ## Send the rendered body Yield the rendering Effect in your application, then supply its body to `Email.send` with explicit sender and recipients. Apply ordinary [provider validation](/providers/capabilities), [submission policy](/reference/options) and any required approval. The [React integration](/integrations/react) shows this composition. These recipes are application examples, not additional template exports from the SDK. Rendering and compilation do not establish delivery or compatibility with every email client. --- # Refund confirmation Source: https://email-sdk.corvencore.com/ui/refund-confirmed This application recipe uses the [shared transactional layout](/ui/components#shared-recipe-layout). It returns a rendering Effect containing HTML and plain text; it does not submit email or implement the business operation. ```ts import { renderReactEmail } from "@corven/email-sdk/react"; import { transactionalEmailLayout } from "./react-layout.js"; export const refundConfirmedEmail = (input: { reference: string; amount: string; detailsUrl: string; }) => renderReactEmail( transactionalEmailLayout({ heading: "Refund confirmation", preview: "Your refund has been recorded.", paragraphs: [ `Refund reference: ${input.reference}.`, `Amount: ${input.amount}. Your payment method may take additional time to show it.`, ], action: { href: input.detailsUrl, label: "View refund" }, }), ); ``` ## Supply authorized data Use the billing system’s confirmed state and settlement wording. An email receipt is not proof that funds have reached the recipient, and the SDK cannot predict bank settlement time. Keep URLs and template inputs under host control. React text escaping is not URL validation or a consent check. Treat the rendered body as private message content. ## Send the rendered body Yield the rendering Effect in your application, then supply its body to `Email.send` with explicit sender and recipients. Apply ordinary [provider validation](/providers/capabilities), [submission policy](/reference/options) and any required approval. The [React integration](/integrations/react) shows this composition. These recipes are application examples, not additional template exports from the SDK. Rendering and compilation do not establish delivery or compatibility with every email client. --- # Team invitation Source: https://email-sdk.corvencore.com/ui/team-invite This application recipe uses the [shared transactional layout](/ui/components#shared-recipe-layout). It returns a rendering Effect containing HTML and plain text; it does not submit email or implement the business operation. ```ts import { renderReactEmail } from "@corven/email-sdk/react"; import { transactionalEmailLayout } from "./react-layout.js"; export const teamInviteEmail = (input: { teamName: string; inviter: string; inviteUrl: string }) => renderReactEmail( transactionalEmailLayout({ heading: "Join your team", preview: "You have a team invitation.", paragraphs: [ `${input.inviter} invited you to ${input.teamName}.`, "Review the invitation before joining.", ], action: { href: input.inviteUrl, label: "Review invitation" }, }), ); ``` ## Supply authorized data Bind the invitation to the intended organization and recipient. Check membership and authorization when the link is used; email delivery itself must not grant access. Keep URLs and template inputs under host control. React text escaping is not URL validation or a consent check. Treat the rendered body as private message content. ## Send the rendered body Yield the rendering Effect in your application, then supply its body to `Email.send` with explicit sender and recipients. Apply ordinary [provider validation](/providers/capabilities), [submission policy](/reference/options) and any required approval. The [React integration](/integrations/react) shows this composition. These recipes are application examples, not additional template exports from the SDK. Rendering and compilation do not establish delivery or compatibility with every email client. --- # Theme email components Source: https://email-sdk.corvencore.com/ui/themes `reactEmailTheme(preset?, overrides?)` returns a frozen `ReactEmailTheme`. Presets are `"light"` and `"dark"`; omitted values use the light preset. Pass the result to `themedReactEmailComponents`. The [rendering example](/integrations/react) customizes the accent and font family. Overrides are partial: omitted fields retain the preset value. | Fields | Use | | ------------------------------ | ---------------------------------- | | `background`, `foreground` | Document background and foreground | | `surface`, `surfaceForeground` | Card and content colors | | `accent`, `accentForeground` | Solid action background and text | | `mutedForeground`, `border` | Secondary text and separators | | `fontFamily` | Host-chosen CSS font stack | | `radius` | Numeric corner radius | ## Choose values deliberately The theme API is typed configuration, not a CSS sanitizer, contrast checker or email-client compatibility layer. Keep values host-owned, check contrast for the colors you choose and use available fallback fonts. Custom fonts are not automatically loaded or attached. A dark preset is a fixed rendered choice; it does not establish that every recipient client will respect the same colors or dark-mode behavior. Inspect the HTML and text outputs in the clients you support. For per-element adjustments, use the component's native `style` or document `bodyStyle` props. Keep layout and content decisions in application templates rather than modifying the shared SDK components. --- # Trial ending Source: https://email-sdk.corvencore.com/ui/trial-ending This application recipe uses the [shared transactional layout](/ui/components#shared-recipe-layout). It returns a rendering Effect containing HTML and plain text; it does not submit email or implement the business operation. ```ts import { renderReactEmail } from "@corven/email-sdk/react"; import { transactionalEmailLayout } from "./react-layout.js"; export const trialEndingEmail = (input: { endsAt: string; reviewUrl: string }) => renderReactEmail( transactionalEmailLayout({ heading: "Your trial is ending", preview: "Review your trial before it ends.", paragraphs: [ `Your trial ends ${input.endsAt}.`, "Review your plan and billing settings before that date.", ], action: { href: input.reviewUrl, label: "Review plan" }, }), ); ``` ## Supply authorized data Supply a human-readable date with an explicit timezone and describe the actual billing behavior. Do not imply a charge, cancellation or renewal unless the billing system has established it. Keep URLs and template inputs under host control. React text escaping is not URL validation or a consent check. Treat the rendered body as private message content. ## Send the rendered body Yield the rendering Effect in your application, then supply its body to `Email.send` with explicit sender and recipients. Apply ordinary [provider validation](/providers/capabilities), [submission policy](/reference/options) and any required approval. The [React integration](/integrations/react) shows this composition. These recipes are application examples, not additional template exports from the SDK. Rendering and compilation do not establish delivery or compatibility with every email client. --- # Usage alert Source: https://email-sdk.corvencore.com/ui/usage-alert This application recipe uses the [shared transactional layout](/ui/components#shared-recipe-layout). It returns a rendering Effect containing HTML and plain text; it does not submit email or implement the business operation. ```ts import { renderReactEmail } from "@corven/email-sdk/react"; import { transactionalEmailLayout } from "./react-layout.js"; export const usageAlertEmail = (input: { usage: string; limit: string; reviewUrl: string }) => renderReactEmail( transactionalEmailLayout({ heading: "Review your usage", preview: "Your account is approaching a usage limit.", paragraphs: [ `Current usage: ${input.usage}. Limit: ${input.limit}.`, "Review usage and available options.", ], action: { href: input.reviewUrl, label: "View usage" }, }), ); ``` ## Supply authorized data Compute usage and thresholds in your application and suppress duplicate alerts for the same logical event. Do not infer that sending an alert changes a quota or subscription. Keep URLs and template inputs under host control. React text escaping is not URL validation or a consent check. Treat the rendered body as private message content. ## Send the rendered body Yield the rendering Effect in your application, then supply its body to `Email.send` with explicit sender and recipients. Apply ordinary [provider validation](/providers/capabilities), [submission policy](/reference/options) and any required approval. The [React integration](/integrations/react) shows this composition. These recipes are application examples, not additional template exports from the SDK. Rendering and compilation do not establish delivery or compatibility with every email client. --- # Verification code Source: https://email-sdk.corvencore.com/ui/verification-code This application recipe uses the [shared transactional layout](/ui/components#shared-recipe-layout). It returns a rendering Effect containing HTML and plain text; it does not submit email or implement the business operation. ```ts import { renderReactEmail } from "@corven/email-sdk/react"; import { transactionalEmailLayout } from "./react-layout.js"; export const verificationCodeEmail = (input: { code: string; expiresIn: string }) => renderReactEmail( transactionalEmailLayout({ heading: "Verify your email", preview: "Your verification code", paragraphs: [`Your verification code is ${input.code}.`, `It expires in ${input.expiresIn}.`], }), ); ``` ## Supply authorized data Issue, expire and rate-limit codes in your authentication system. Render only the code for the authorized recipient. Never put it in a subject line, URL, telemetry event or model prompt. Keep URLs and template inputs under host control. React text escaping is not URL validation or a consent check. Treat the rendered body as private message content. ## Send the rendered body Yield the rendering Effect in your application, then supply its body to `Email.send` with explicit sender and recipients. Apply ordinary [provider validation](/providers/capabilities), [submission policy](/reference/options) and any required approval. The [React integration](/integrations/react) shows this composition. These recipes are application examples, not additional template exports from the SDK. Rendering and compilation do not establish delivery or compatibility with every email client. --- # Welcome Source: https://email-sdk.corvencore.com/ui/welcome This application recipe uses the [shared transactional layout](/ui/components#shared-recipe-layout). It returns a rendering Effect containing HTML and plain text; it does not submit email or implement the business operation. ```ts import { renderReactEmail } from "@corven/email-sdk/react"; import { transactionalEmailLayout } from "./react-layout.js"; export const welcomeEmail = (input: { name: string; startUrl: string }) => renderReactEmail( transactionalEmailLayout({ heading: "Welcome", preview: "Your account is ready.", paragraphs: [`Hello ${input.name}, your account is ready.`, "Choose your first step below."], action: { href: input.startUrl, label: "Get started" }, }), ); ``` ## Supply authorized data Choose a useful authenticated destination. Keep account creation and marketing consent separate; a welcome notification is not permission to subscribe a contact to a campaign. Keep URLs and template inputs under host control. React text escaping is not URL validation or a consent check. Treat the rendered body as private message content. ## Send the rendered body Yield the rendering Effect in your application, then supply its body to `Email.send` with explicit sender and recipients. Apply ordinary [provider validation](/providers/capabilities), [submission policy](/reference/options) and any required approval. The [React integration](/integrations/react) shows this composition. These recipes are application examples, not additional template exports from the SDK. Rendering and compilation do not establish delivery or compatibility with every email client.