Documentation for agents: an index at /llms.txt, every docs page in one file at /llms-full.txt, and Markdown for any docs page by adding .md to its URL.

Effect KitEmails

Idempotency

Pass an idempotencyKey so a repeated send does not deliver the same message twice, on the providers that support it.

To send the same message again without delivering it twice, pass an idempotencyKey in the second argument of email.send. The provider uses the key to recognize the repeat.

Example (Sending With a Key on Resend)

import {  } from "effect";
import {  } from "@effect-kit/emails";
import * as  from "@effect-kit/emails/resend";

const  = .(function* () {
  const  = yield* ;
  return yield* .(
    {
      : "hello@example.com",
      : "ada@example.com",
      : "Your receipt",
      : "Thanks for your order.",
    },
    { : "receipt/order-1042" },
  );
}).(.(.()));

Use one key per message, derived from something stable such as an order id. A key made fresh on each attempt protects nothing.

Which providers support it

Four providers take a key. Every other provider fails the send before any request.

ProviderModeRetentionKey rules
resendreplay24 hoursAt most 256 characters.
lettermintreplay24 hours1 to 255 printable ASCII characters, not blank.
eusendreplaynot declared1 to 255 printable ASCII characters, not blank.
brevoreject30 minutesMust be a UUID.

The modes differ in what a repeated key does:

  • replay: the provider treats the repeat as the same send and does not deliver a second message.
  • reject: the provider refuses the repeat, and the send fails with IdempotencyConflictError.

retention in email.capabilities.idempotency is the window the package declares. It is a hint for your code. The package does not enforce it.

A key that breaks the rules fails with InvalidMessageError and acceptance: "notSubmitted", with idempotencyKey in paths. Brevo also rejects a custom header named idempotencyKey, because the package sends the key under that name.

Handling a conflict

IdempotencyConflictError means the provider saw the key before and did not accept the new request as a fresh send. The package maps these provider signals to it:

ProviderSignal
resendinvalid_idempotent_request, concurrent_idempotent_requests
lettermintA ConflictError from the SDK.
eusendThe CONFLICT code.
brevoThe duplicate_parameter code.

Example (Treating a Repeated Key as Already Sent)

import { Effect } from "effect";
import { Email } from "@effect-kit/emails";

const sendReceipt = Effect.gen(function* () {
  const email = yield* Email;
  return yield* email.send(
    {
      from: "hello@example.com",
      to: "ada@example.com",
      subject: "Your receipt",
      text: "Thanks for your order.",
    },
    { idempotencyKey: "receipt/order-1042" },
  );
}).pipe(Effect.catchReason("EmailError", "IdempotencyConflictError", () => Effect.void));

On Resend a conflict arrives with acceptance: "unknown", so a conflict is not proof that the first send succeeded.

Providers without support

An idempotencyKey on a provider with mode unsupported fails before any request, with UnsupportedFeatureError and feature: "idempotency".

The key is not ignored

Passing a key to Postmark, SES or any provider outside the table above does not degrade to a plain send. It fails with acceptance: "notSubmitted". Read email.capabilities.idempotency.mode first, as in Capabilities.

Keys in batches

Email.sendMany keeps the key you set on each request and never generates one. A request without a key is sent without one.

Example (Keying Each Message in a Batch)

import { Email } from "@effect-kit/emails";

const orders = ["1042", "1043"];

const program = Email.sendMany(
  orders.map((id) => ({
    message: {
      from: "hello@example.com",
      to: "ada@example.com",
      subject: `Receipt ${id}`,
      text: "Thanks for your order.",
    },
    options: { idempotencyKey: `receipt/order-${id}` },
  })),
);