Email SDK
Adapters

Delivery capabilities

Decide when an adapter can preserve repeated headers, idempotency, scheduled delivery, or personalized fanout.

Field support answers whether an adapter can represent a message field. Capabilities answer how delivery behaves when retries, duplicate risk, timing, or recipient fanout matter.

Choose by delivery requirement

  • Preserve repeated header names with adapters whose repeatedHeaders capability is true; duplicate names survive instead of failing validation.
  • Use provider-side idempotency when idempotency is native; message_id means SMTP derives a stable Message-ID, but the SMTP server still decides deduplication.
  • Use provider-side scheduled delivery when scheduling is true; sendAt is translated to the provider, and the SDK never waits or queues.
  • Use one native personalized request when personalized is native; every other built-in adapter expands personalized delivery into deterministic sequential one-recipient sends.
Delivery behavior by built-in adapter. Unsupported message fields still fail field validation first.
Repeated headers
No
Idempotency
Native
Scheduling
Yes
Personalized fanout
Expanded
Repeated headers
Yes
Idempotency
None
Scheduling
No
Personalized fanout
Expanded
Repeated headers
No
Idempotency
None
Scheduling
Yes
Personalized fanout
Native
Repeated headers
No
Idempotency
None
Scheduling
No
Personalized fanout
Expanded
Repeated headers
No
Idempotency
None
Scheduling
No
Personalized fanout
Expanded
Repeated headers
Yes
Idempotency
None
Scheduling
No
Personalized fanout
Expanded
Repeated headers
Yes
Idempotency
None
Scheduling
Yes
Personalized fanout
Native
Repeated headers
No
Idempotency
None
Scheduling
Yes
Personalized fanout
Expanded
Repeated headers
No
Idempotency
None
Scheduling
Yes
Personalized fanout
Expanded
Repeated headers
No
Idempotency
None
Scheduling
Yes
Personalized fanout
Expanded
Repeated headers
No
Idempotency
None
Scheduling
Yes
Personalized fanout
Expanded
Repeated headers
No
Idempotency
None
Scheduling
No
Personalized fanout
Expanded
Repeated headers
No
Idempotency
None
Scheduling
No
Personalized fanout
Expanded
Repeated headers
No
Idempotency
None
Scheduling
No
Personalized fanout
Expanded
Repeated headers
No
Idempotency
Native
Scheduling
No
Personalized fanout
Expanded
Repeated headers
No
Idempotency
Native
Scheduling
No
Personalized fanout
Expanded
Repeated headers
No
Idempotency
Native
Scheduling
No
Personalized fanout
Expanded
Repeated headers
No
Idempotency
None
Scheduling
No
Personalized fanout
Expanded
Repeated headers
No
Idempotency
None
Scheduling
No
Personalized fanout
Expanded
Repeated headers
Yes
Idempotency
None
Scheduling
No
Personalized fanout
Expanded
Repeated headers
No
Idempotency
None
Scheduling
No
Personalized fanout
Expanded
Repeated headers
No
Idempotency
None
Scheduling
No
Personalized fanout
Expanded
Repeated headers
Yes
Idempotency
Message-ID
Scheduling
No
Personalized fanout
Expanded

Every fallback candidate must preserve the message's fields and required delivery capabilities. No built-in adapter silently ignores an unsupported capability.

Scheduling is not a queue

Use sendAt for a simple future send when the selected provider supports the required time window. Use a durable scheduler or workflow when you need provider independence, long horizons, cancellation, rescheduling, persisted state, or recovery after process failure.

A cron job is only a polling implementation. If an application uses one, store due jobs in a database, claim them with a lease or transaction, and pass a stable idempotency key to email.send. Do not keep timers or a cron loop inside Email SDK.

Idempotency does not make fallback exactly once

One key cannot guarantee exactly-once delivery across different providers. When a provider returns delivery: "unknown", v1 stops fallback by default because a second provider may create a duplicate.

src/email.ts
const email = createEmailClient({
  adapters: [primary, backup],
  fallback: {
    adapters: ["backup"],
    onUnknownDelivery: "stop",
  },
});

Validate the complete route

Validate before a durable queue accepts the job:

src/jobs/enqueue-email.ts
await email.validate(message, {
  fallback: { adapters: ["backup"] },
});

Use Field support for address, attachment, tag, metadata, and scheduling field compatibility, and Production pipeline for queueing and recovery boundaries.

On this page