What it does

strapi-provider-email-volanea routes Strapi's Email plugin through the Volanea API. Every message Strapi sends on its own — admin invites, password resets, user confirmation emails — plus anything you send yourself with strapi.plugins.email.services.email.send() goes out from a domain you have verified, with delivery, bounce and complaint events streaming back.

Zero runtime dependencies. Strapi v5, Node 18+. It speaks HTTP, not SMTP: there is no port to unblock and no SMTP password stored in your project.

Before you install

Verify a sending domain. In the Volanea dashboard, Domains → Add domain, then copy the DKIM, SPF and DMARC records into your DNS exactly as shown. Until the domain reads verified, every send returns 403 — this is the single most common reason a freshly installed provider "doesn't work".

Two DNS traps worth knowing:

  • A wildcard record like *.yourdomain.com shadows the _domainkey lookups. Add the DKIM hosts explicitly.
  • You get one SPF record per domain. Merge Volanea's include: into your existing one; two TXT records is a permanent failure, not a warning.

Get your key. Dashboard → Settings → API keys. The secret key (sk_…) is what the provider authenticates with. A test key runs the whole pipeline and delivers nothing — useful while you wire things up.

Install

npm install strapi-provider-email-volanea

Configure

config/plugins.js:

module.exports = ({ env }) => ({
  email: {
    config: {
      provider: 'strapi-provider-email-volanea',
      providerOptions: {
        apiKey: env('VOLANEA_API_KEY'),
      },
      settings: {
        defaultFrom: env('VOLANEA_DEFAULT_FROM', 'no-reply@yourdomain.com'),
        defaultFromName: env('VOLANEA_DEFAULT_FROM_NAME', 'Your App'),
        defaultReplyTo: env('VOLANEA_DEFAULT_REPLY_TO', 'support@yourdomain.com'),
      },
    },
  },
});

TypeScript projects use the same shape in config/plugins.ts with export default.

.env:

VOLANEA_API_KEY=sk_live_xxxxxxxxxxxx
VOLANEA_DEFAULT_FROM=no-reply@yourdomain.com
VOLANEA_DEFAULT_FROM_NAME=Your App
VOLANEA_DEFAULT_REPLY_TO=support@yourdomain.com

A missing or malformed option throws while Strapi is booting rather than failing silently at the first password reset — a misconfigured provider should stop the build, not lose mail.

Send

await strapi.plugins.email.services.email.send({
  to: 'customer@example.com',
  subject: 'Welcome aboard',
  html: '<p>Thanks for signing up.</p>',
  text: 'Thanks for signing up.',
});

Stored Volanea templates work too — pass templateId and variables instead of inline content:

await strapi.plugins.email.services.email.send({
  to: 'customer@example.com',
  templateId: 'tpl_welcome',
  variables: { firstName: 'Ada' },
});

cc and bcc are accepted. Volanea sends one discrete message per recipient, so copied addresses each get their own copy and never see one another — bcc semantics, applied to both.

Attachments

await strapi.plugins.email.services.email.send({
  to: 'customer@example.com',
  subject: 'Your invoice',
  html: '<p>Invoice attached.</p>',
  attachments: [
    { filename: 'invoice.pdf', content: pdfBuffer, contentType: 'application/pdf' },
  ],
});

Up to 10 attachments, 10 MiB per request. Nothing is read from disk or from a URL — pass the bytes.

A string content is treated as raw text, the same way Nodemailer treats it, because that is what Strapi's Email plugin passes through. If you already hold encoded bytes, say so, or they will be encoded a second time:

{ filename: 'invoice.pdf', content: base64String, encoding: 'base64' }
// or
{ filename: 'invoice.pdf', content: 'data:application/pdf;base64,JVBERi0…' }

encoding accepts any Node buffer encoding. (Worth knowing if you also use the Medusa provider: there a string is treated as already base64, because that is Medusa's own convention. The two frameworks genuinely disagree.)

Options

providerOptions:

OptionDefaultDescription
apiKeyRequired. Volanea secret key.
baseUrlhttps://api.volanea.comOverride the API host.
timeout15000Request timeout in ms.
throwOnSkiptrueThrow when every recipient was skipped.

settings: defaultFrom, defaultFromName, defaultReplyTo, defaultSubject, defaultType.

Two behaviours to design around

A 200 does not mean delivered. The API returns as soon as the message is durably queued; the provider hand-off happens afterwards. The provider reports accepted, never delivered. For the real outcome, register a webhook for email.delivered, email.bounced and email.complained.

A 200 can mean nothing was sent. Volanea reports per-recipient skips — suppressed, unsubscribed, over quota, reputation-paused — inside a successful response. Strapi treats a resolved promise as success, so a fully skipped send would otherwise vanish silently, and a user would never get their password reset.

So when every recipient was skipped, the provider throws (all_recipients_skipped). Set throwOnSkip: false if you would rather the send resolve and read result.skipped yourself.

Errors

Branch on the error's code, never on the wording.

CodeMeaning
unauthorizedMissing or invalid API key.
send_failedSend rejected — an unverified from domain is the usual cause.
template_not_foundThe templateId does not exist.
validation_errorMalformed request; failing fields are in details.
rate_limitedPer-project burst cap.
all_recipients_skippedAccepted, but nobody was mailed.
invalid_attachmentAn attachment had no content.
timeout / network_errorThe API did not answer.

A raw fetch error never reaches Strapi — transport failures are wrapped before they leave the provider.

Troubleshooting

403 on every send. The from domain is not verified. Check Domains in the dashboard; defaultFrom must sit on a verified domain.

Nothing arrives and there is no error. You are running with throwOnSkip: false and the recipients were skipped. Check result.skipped, then Suppressions in the dashboard — a suppressed address stays suppressed until you remove it.

It works in development but not production. Almost always a test-mode key. Test sends render, validate and log, but never leave the building.

global fetch is unavailable. Node 16 or older. Strapi v5 requires Node 18+.

Admin invites still come from Strapi's default sender. Another email provider is still configured, or config/plugins.js was not picked up for the current environment — check config/env/<env>/plugins.js too.

Testing locally

Send to success@simulator.amazonses.com. The SES mailbox simulator absorbs it: never bounces, never counts against your quota or sender reputation.

Do not invent a test address on a real domain. It hard-bounces, and hard bounces damage the sender reputation of every domain on your account.

Source