Payload CMS
Send Payload's verification, password-reset and application email through eusend with the official email adapter — a REST adapter that works on serverless, unlike SMTP.
Payload sends email through an email adapter: a small object in your
payload.config.ts that every payload.sendEmail() call, and every built-in
auth email, goes through. @eusend_dev/payload-email is the eusend adapter.
It calls the eusend REST API rather than SMTP, so it works in serverless
functions — where holding an SMTP connection open is the usual reason
@payloadcms/email-nodemailer misbehaves.
Installation
npm install @eusend_dev/payload-email
# or
bun add @eusend_dev/payload-emailSetup
Verify your sending domain
Add your domain under Domains and publish the DNS records — the adapter can only send from a verified domain. See domains if you have not done this yet.
Add the API key
Create a key under API keys and put it in your environment:
EUSEND_API_KEY=eu_live_...Configure the adapter
import { buildConfig } from 'payload'
import { eusendAdapter } from '@eusend_dev/payload-email'
export default buildConfig({
email: eusendAdapter({
apiKey: process.env.EUSEND_API_KEY || '',
defaultFromAddress: 'hello@yourdomain.com',
defaultFromName: 'Your app',
}),
// ...the rest of your config
})Options
| Parameter | Type | Description |
|---|---|---|
apiKeyrequired | string | An eusend API key. |
defaultFromAddressrequired | string | Sender used when a message sets no from. Its domain must be verified on your account. |
defaultFromNamerequired | string | Display name paired with defaultFromAddress. |
overrideRecipientAddress | string | Redirect every message to this address instead of its real recipients. cc and bcc are dropped along with them, so a staging environment cannot copy a real customer. |
tags | Record<string, string> | Tags added to every send, e.g. { source: "payload" }. They filter the email log and are returned on the webhook events for the send. |
trackOpens | boolean | Override open tracking for messages sent through this adapter. |
trackClicks | boolean | Override click tracking for messages sent through this adapter. |
baseUrl | string | Override the API host. Only useful against a staging deployment. |
Tagging Payload's email
Tags let you tell Payload's traffic apart from the rest of your sending in the email log, and route it in webhooks:
email: eusendAdapter({
apiKey: process.env.EUSEND_API_KEY || '',
defaultFromAddress: 'hello@yourdomain.com',
defaultFromName: 'Your app',
tags: { source: 'payload' },
})Staging without mailing customers
overrideRecipientAddress sends everything to one inbox. Unlike the to-only
overrides some adapters ship, cc and bcc are dropped with the real
recipients — a redirected message that still copies the customer is not a
redirected message.
email: eusendAdapter({
apiKey: process.env.EUSEND_API_KEY || '',
defaultFromAddress: 'hello@yourdomain.com',
defaultFromName: 'Your app',
overrideRecipientAddress: process.env.STAGING_INBOX,
})Attachments
payload.sendEmail() takes nodemailer's attachment shape. All four forms work:
await payload.sendEmail({
to: 'customer@example.com',
subject: 'Your invoice',
html: '<p>Attached.</p>',
attachments: [
{ filename: 'invoice.pdf', content: pdfBuffer },
{ filename: 'notes.txt', content: 'plain text' },
{ path: 'https://example.com/invoice.pdf' },
{ path: '/var/data/invoice.pdf' },
],
})A URL is fetched by eusend at send time; a filesystem path is read by the
adapter before the request. A string content is treated as UTF-8 text unless
you pass encoding: 'base64' — the same rule nodemailer applies, so an
attachment ported over from @payloadcms/email-nodemailer arrives intact.
Set cid on an attachment to reference it from your HTML as
<img src="cid:invoice-logo"> instead of showing it as a download.
Errors
A failed send throws Payload's APIError, carrying the HTTP status and the
eusend error code:
Error sending email: DOMAIN_NOT_VERIFIED - Domain not verifiedThe two you are most likely to meet first are DOMAIN_NOT_VERIFIED, when
defaultFromAddress is on a domain that has not finished verifying, and
ALL_SUPPRESSED, when every recipient is on your
suppression list.
Coming from another adapter
Swapping @payloadcms/email-resend for eusend is the import, the factory name
and the key:
- import { resendAdapter } from '@payloadcms/email-resend'
+ import { eusendAdapter } from '@eusend_dev/payload-email'
export default buildConfig({
- email: resendAdapter({
+ email: eusendAdapter({
defaultFromAddress: 'hello@yourdomain.com',
defaultFromName: 'Your app',
- apiKey: process.env.RESEND_API_KEY || '',
+ apiKey: process.env.EUSEND_API_KEY || '',
}),
})From @payloadcms/email-nodemailer, replace the whole transportOptions block
with the three fields above — the defaultFromAddress and defaultFromName you
already have keep their meaning, and your payload.sendEmail() calls do not
change.
If you would rather keep nodemailer, eusend also speaks SMTP: see the nodemailer guide.