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.1The SDK is a pinned optional peer. It is needed only when you import this subpath.
Setup
Resend.layerConfig() reads these variables:
| Variable | Required | Value |
|---|---|---|
RESEND_API_KEY | yes | API 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:
| Text | HTML | Both | Sender name | Cc | Bcc | Reply-to | Headers | Attachments | Inline images | Idempotency |
|---|---|---|---|---|---|---|---|---|---|---|
| yes | yes | yes | yes | yes | yes | multiple | yes | yes | yes | replay, 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.
field | Raised when |
|---|---|
apiKey | The key is not printable ASCII without spaces. |
baseUrl | The URL is not http or https, or has credentials, a query or a fragment. |
timeout | The timeout is not finite and positive. |
| Resend response | Reason | acceptance |
|---|---|---|
Status 500 or above, service_unavailable, internal_server_error | ProviderUnavailableError | unknown |
invalid_idempotent_request, concurrent_idempotent_requests | IdempotencyConflictError | unknown |
Status 401, missing_api_key, invalid_api_key | AuthenticationError | rejected |
daily_quota_exceeded, monthly_quota_exceeded, email_above_quota | QuotaExceededError | rejected |
Status 403, restricted_api_key, invalid_permission, suspended_api_key, invalid_access, security_error | PermissionDeniedError | rejected |
Status 429, rate_limit_exceeded | RateLimitedError | rejected |
validation_error, invalid_idempotency_key, invalid_attachment, invalid_from_address, invalid_parameter, missing_required_field, missing_required_parameter | InvalidMessageError | rejected |
application_error with no status | TransportError | unknown |
| Anything else | ProviderError | unknown |
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.