# eusend

> For the complete documentation index, see [llms.txt](https://effectkit.dev/llms.txt).

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

Source: https://effectkit.dev/docs/emails/providers/eusend

The `@effect-kit/emails/eusend` subpath sends messages through [eusend](https://eusend.dev/docs) and provides the `Email` service.

## Installation [#installation]

```package-install
@effect-kit/emails effect @eusend_dev/sdk@0.19.0
```

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

## Setup [#setup]

`Eusend.layerConfig()` reads these variables:

| Variable         | Required | Value   |
| ---------------- | -------- | ------- |
| `EUSEND_API_KEY` | yes      | API key |

**Example** (Sending a Message With eusend)

```ts twoslash
import { ConfigProvider, Effect, Layer } from "effect";
import { Email } from "@effect-kit/emails";
import * as Eusend from "@effect-kit/emails/eusend";

const EmailLive = Eusend.layerConfig().pipe(
  Layer.provide(ConfigProvider.layer(ConfigProvider.fromEnv())),
);

const program = Effect.gen(function* () {
  const email = yield* Email;
  return yield* email.send({
    from: "hello@example.com",
    to: "ada@example.com",
    subject: "Welcome",
    text: "Your account is ready.",
  });
});

Effect.runPromise(program.pipe(Effect.provide(EmailLive)));
```

Use a `from` address your eusend account is allowed to send from.

## Options [#options]

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

<TypeTable
  type={{
    apiKey: {
      type: "Redacted<string>",
      description: "The API key. Must be printable ASCII with no spaces.",
      required: true,
    },
    baseUrl: {
      type: "URL",
      description: "API origin. Must be http or https with no credentials, query or fragment.",
      default: "https://api.eusend.dev",
      required: false,
    },
    timeout: {
      type: "Duration",
      description:
        "Time allowed for one send. Must be finite and positive. When it elapses the send fails with TimeoutError.",
      default: "30 seconds",
      required: false,
    },
  }}
/>

## Capabilities [#capabilities]

eusend 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      |

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

## Idempotency [#idempotency]

Mode `replay`, with no retention declared. The package passes the key to eusend as the SDK's `idempotencyKey` option.

The key must be 1 to 255 printable ASCII characters and not blank, otherwise the send fails with `InvalidMessageError` (path `idempotencyKey`) and `acceptance: "notSubmitted"`. A `CONFLICT` error from eusend fails with `IdempotencyConflictError`.

**Example** (Sending With an Idempotency Key)

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

const program = Effect.gen(function* () {
  const email = yield* Email;
  return yield* email.send(
    {
      from: "hello@example.com",
      to: "ada@example.com",
      subject: "Welcome",
      text: "Your account is ready.",
    },
    { idempotencyKey: "welcome-ada-2026-10-29" },
  );
});
```

## Provider notes [#provider-notes]

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

## Using your own client [#using-your-own-client]

`layerClient` takes any object with `emails.send(message, { idempotencyKey })`, which an `Eusend` client has. `options` is optional.

**Example** (Borrowing a eusend Client)

```ts
import { Eusend as EusendSdk } from "@eusend_dev/sdk";
import * as Eusend from "@effect-kit/emails/eusend";

const client = new EusendSdk("fake-key");

const EmailLive = Eusend.layerClient(client);
```

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

## Errors [#errors]

`ProviderConfigurationError` carries `provider: "eusend"` and a `field` naming the bad option. Layer construction fails with it, and `layerClient` fails with it for the client options.

| `field`   | Raised when                                                              |
| --------- | ------------------------------------------------------------------------ |
| `timeout` | The timeout is not finite and positive.                                  |
| `token`   | The API key is not printable ASCII without spaces.                       |
| `baseUrl` | The URL is not http or https, or has credentials, a query or a fragment. |

| eusend error name                                                                                                           | Reason                     |
| --------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
| `UNAUTHORIZED`                                                                                                              | `AuthenticationError`      |
| `FORBIDDEN`, `DOMAIN_NOT_VERIFIED`, `SENDING_SUSPENDED`, `ACCOUNT_RESTRICTED`, `WORKSPACE_INACTIVE`, `SENDER_NOT_PERMITTED` | `PermissionDeniedError`    |
| `MONTHLY_LIMIT_EXCEEDED`, `DAILY_LIMIT_EXCEEDED`, `PLAN_LIMIT_EXCEEDED`                                                     | `QuotaExceededError`       |
| `ALL_SUPPRESSED`, `RECIPIENT_DOMAIN_UNDELIVERABLE`                                                                          | `MessageRejectedError`     |
| `RATE_LIMITED`                                                                                                              | `RateLimitedError`         |
| `CONFLICT`                                                                                                                  | `IdempotencyConflictError` |
| `application_error`                                                                                                         | `TransportError`           |
| `SERVICE_PAUSED`, `ATTACHMENT_STORAGE_ERROR`, `INTERNAL_ERROR`                                                              | `ProviderUnavailableError` |

The error name is kept as `providerCode` and its status as `statusCode`. Names not listed use the status with the shared mapping.
