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

Resend

Send email through Resend with the @effect-kit/emails/resend Layer.

The @effect-kit/emails/resend subpath sends messages through Resend and provides the Email service.

Installation

npm install @effect-kit/emails effect resend@6.34.1

The SDK is a pinned optional peer. It is needed only when you import this subpath.

Setup

Resend.layerConfig() reads these variables:

VariableRequiredValue
RESEND_API_KEYyesAPI key

Example (Sending a Message With Resend)

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

const  = .().(
  .(.(.())),
);

const  = .(function* () {
  const  = yield* ;
  return yield* .({
    : "hello@example.com",
    : "ada@example.com",
    : "Welcome",
    : "Your account is ready.",
  });
});

.(.(.()));

Use a from address your Resend account is allowed to send from.

Options

Resend.layer takes these options. layerConfig builds the same object from a Config.

Prop

Type

Capabilities

Resend reports this in email.capabilities:

TextHTMLBothSender nameCcBccReply-toHeadersAttachmentsInline imagesIdempotency
yesyesyesyesyesyesmultipleyesyesyesreplay, 24 hours

Every message feature is supported, so no message fails the capability check.

Idempotency

Mode replay, with a retention of 24 hours. The package passes the key to Resend as the SDK's idempotencyKey option. A key longer than 256 characters fails with InvalidMessageError (path idempotencyKey) before sending.

When Resend reports invalid_idempotent_request or concurrent_idempotent_requests, the send fails with IdempotencyConflictError and acceptance: "unknown".

Example (Sending With an Idempotency Key)

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

const  = .(function* () {
  const  = yield* ;
  return yield* .(
    {
      : "hello@example.com",
      : "ada@example.com",
      : "Welcome",
      : "Your account is ready.",
    },
    { : "welcome-ada-2026-10-29" },
  );
});

Provider notes

The receipt's requestId comes from the x-request-id response header when Resend returns one.

Using your own client

layerClient takes any object with emails.send(message, { signal, idempotencyKey }), which the Resend SDK client has. options is optional.

Example (Borrowing a Resend Client)

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

const client = new ResendSdk("re_fake_key");

const EmailLive = Resend.layerClient(client);

You own a borrowed client. The Layer never closes it, and the client keeps its own retry, timeout and logging policy.

Errors

Layer construction fails with ResendConfigurationError, whose field names the bad option. layerClient fails with it for the client options.

fieldRaised when
apiKeyThe key is not printable ASCII without spaces.
baseUrlThe URL is not http or https, or has credentials, a query or a fragment.
timeoutThe timeout is not finite and positive.
Resend responseReasonacceptance
Status 500 or above, service_unavailable, internal_server_errorProviderUnavailableErrorunknown
invalid_idempotent_request, concurrent_idempotent_requestsIdempotencyConflictErrorunknown
Status 401, missing_api_key, invalid_api_keyAuthenticationErrorrejected
daily_quota_exceeded, monthly_quota_exceeded, email_above_quotaQuotaExceededErrorrejected
Status 403, restricted_api_key, invalid_permission, suspended_api_key, invalid_access, security_errorPermissionDeniedErrorrejected
Status 429, rate_limit_exceededRateLimitedErrorrejected
validation_error, invalid_idempotency_key, invalid_attachment, invalid_from_address, invalid_parameter, missing_required_field, missing_required_parameterInvalidMessageErrorrejected
application_error with no statusTransportErrorunknown
Anything elseProviderErrorunknown

retryAfter is read from the retry-after header on rate limit, quota and unavailable failures. The Resend error name is kept as providerCode when it is one of the names above and the status as statusCode.