Send email from Drupal reliably by treating the form submission and the email send as two separate jobs: Drupal records the submission first, then a server-side queue worker calls Volanea’s REST API. That design protects the visitor experience, keeps credentials out of the browser, and gives you a clear place to handle retries without sending duplicate messages.

This guide uses the contributed Webform module as the concrete Drupal trigger. It does not assume that Volanea has a Drupal marketplace module or native Drupal app. Instead, it uses capabilities Drupal actually provides: Webform submissions are stored as webform_submission entities, Drupal core exposes a Guzzle-based http_client service for outbound HTTP, and Drupal’s Queue API can defer API work until after the form request completes.

What this Drupal integration does

The flow in this guide is intentionally narrow and dependable:

  1. A visitor submits a Drupal Webform whose machine name is contact_request.
  2. Webform creates a new webform_submission entity.
  3. hook_webform_submission_insert() runs after that submission is saved.
  4. The hook places a small job on a Drupal queue instead of calling an email API during the visitor’s request.
  5. A queue worker loads the saved submission, maps its machine-name fields to a Volanea message payload, and sends POST https://api.volanea.com/v1/send.
  6. The worker supplies a stable Idempotency-Key based on the submission ID, so a retry remains one logical email send.

The concrete trigger is therefore a newly created Webform submission. In Drupal’s Webform model, the submission is an entity, and its submitted answers are available as an associative data array keyed by the Webform element machine names. For a contact form, those might be email, first_name, topic, and message.

This is a transactional-email workflow. It is appropriate for acknowledgements, internal contact-form alerts, application-received notices, quote requests, account notifications, and similar messages caused by one person’s action. It is not a substitute for a consent-aware marketing workflow.

Why use a custom Drupal module instead of a native plugin

There is no native Volanea Drupal app, marketplace listing, or official Drupal module to install. That is not a blocker: Drupal can make outbound HTTP requests through its core http_client service, which is a Guzzle client, and custom modules can react to entity lifecycle hooks.

The benefit of a small custom module is control. Your integration has an explicit trigger, field mapping, sender identity, error policy, and audit trail. It also avoids a common security mistake: attempting to call an email API from JavaScript embedded in a public Drupal page.

A browser-side request would expose the Volanea API key to anyone who can inspect page source, browser developer tools, a network trace, or a bundled JavaScript file. That key could then be used to send unwanted mail, damage sender reputation, or consume account capacity. The only safe place for the key is server-side.

For most sites, the module described here has only three responsibilities:

  • detect a completed, newly inserted submission for the one Webform you intend to automate;
  • queue a job that identifies the saved submission; and
  • send the mapped message from a queue worker using the server-side API key.

That is deliberately less magical than a generic automation chain. It is also easier to test and safer to operate when the email matters.

The Drupal Webform trigger and payload shape

Create a Webform with the machine name contact_request. Its visible labels can be anything, but the element machine names in this example must match the code:

  • email — required email element for the visitor’s address.
  • first_name — optional text field.
  • topic — optional select or text field.
  • message — required textarea.

When a visitor submits it, Webform stores data in a structure conceptually like this:

$submission_data = [
  'email' => 'maya@example.com',
  'first_name' => 'Maya',
  'topic' => 'Implementation question',
  'message' => 'Could you tell me whether the product supports SSO?',
];

That is Drupal’s submission-data shape: an associative PHP array keyed by element machine names. The values are not automatically an email-provider payload, and they should not be forwarded blindly. Your module must choose what to send, validate required values, and escape user-provided content before putting it into HTML.

The corresponding Volanea request body in this guide is:

{
  "from": "Website Team <hello@updates.example.com>",
  "to": ["maya@example.com"],
  "subject": "We received your implementation question",
  "html": "<p>Hi Maya,</p><p>We received your message.</p>",
  "text": "Hi Maya,\n\nWe received your message.",
  "tags": ["drupal", "contact-request"]
}

Volanea’s single-message endpoint is POST /v1/send. It accepts one recipient or a recipient array of up to 50 addresses, supports Bearer authentication, and supports an Idempotency-Key header for safe retries. For this acknowledgement workflow, one recipient per submission is the clearest design: it avoids accidentally exposing one submitter’s address to another.

Before testing, verify the sender domain in Volanea and use an address on that authenticated domain for from. A message request can be valid JSON yet still fail operationally if the sender identity is not configured for the domain you are trying to use. The email API reference and setup guides are the right place to confirm the current endpoint contract and domain setup before production deployment.

Build the module around a queue, not the form response

It is technically possible to call Volanea immediately inside a submission hook. Do not make that your default for public forms. A remote HTTP call can be slow, can time out after Volanea has accepted the request, or can fail because of a temporary network issue. If that work happens inside the form submission request, the visitor may see a slow page or an error even though Drupal successfully saved their message.

Queueing changes the failure boundary. Drupal records the submission, then stores a lightweight queue item containing the submission ID. The user receives the normal Webform confirmation independently of email-delivery timing. A later cron run processes the queue item and can retry work that throws an exception.

Create a custom module named volanea_drupal. Its volanea_drupal.info.yml can be minimal:

name: Volanea Drupal Email
 type: module
 description: Sends transactional acknowledgements after selected Webform submissions.
 package: Custom
 core_version_requirement: ^10 || ^11
 dependencies:
   - webform:webform

The extra space before type should be removed in your actual file; YAML keys must start at the left margin. The intended file is:

name: Volanea Drupal Email
type: module
description: Sends transactional acknowledgements after selected Webform submissions.
package: Custom
core_version_requirement: ^10 || ^11
dependencies:
  - webform:webform

Enable it with your normal deployment process. For a Composer-managed Drupal site, put the module under web/modules/custom/volanea_drupal or the equivalent custom-module directory configured by your project.

Queue a newly created submission

The following volanea_drupal.module code uses Webform’s submission insert hook. It filters by the Webform machine name, ignores incomplete submissions, and queues the submission ID. It does not put the entire message, email address, or Volanea key into the queue payload.

<?php

use Drupal\webform\WebformSubmissionInterface;

/**
 * Implements hook_webform_submission_insert().
 */
function volanea_drupal_webform_submission_insert(WebformSubmissionInterface $webform_submission): void {
  // Only react to a newly created, completed submission from this Webform.
  if ($webform_submission->getWebform()->id() !== 'contact_request') {
    return;
  }

  if (!$webform_submission->isCompleted()) {
    return;
  }

  // Store only the durable identifier. The worker will load current data.
  \Drupal::queue('volanea_drupal_contact_ack')->createItem([
    'submission_id' => (int) $webform_submission->id(),
  ]);
}

The important detail is the lifecycle event: the hook runs after a new Webform submission has been inserted. It does not run for an ordinary edit of the same submission. If your process allows drafts, do not send an acknowledgement on draft creation. The isCompleted() check makes the intent explicit, but you should test it against your exact Webform settings, especially if editors or custom code can create submissions programmatically.

For workflows driven by content rather than forms, the same architecture still applies. You would swap the trigger for a node entity insert/update hook, or an event tied to the moderation transition your site uses. Keep the queue and the sending service separate from the business trigger. That separation is what makes it possible to change “form submitted” to “application moved to approved” without rewriting email delivery.

Send the Volanea REST request from a QueueWorker

Create src/Plugin/QueueWorker/VolaneaContactAcknowledgement.php. This worker loads the saved Webform submission, extracts values by field machine name, maps them into the Volanea request, and makes the outbound request with Drupal’s http_client service.

<?php

namespace Drupal\volanea_drupal\Plugin\QueueWorker;

use Drupal\Component\Utility\Html;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Queue\QueueWorkerBase;
use Drupal\webform\WebformSubmissionStorageInterface;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Sends a Webform acknowledgement through Volanea.
 *
 * @QueueWorker(
 *   id = "volanea_drupal_contact_ack",
 *   title = @Translation("Volanea contact acknowledgement"),
 *   cron = {"time" = 60}
 * )
 */
final class VolaneaContactAcknowledgement extends QueueWorkerBase implements ContainerFactoryPluginInterface {

  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    private readonly WebformSubmissionStorageInterface $submissionStorage,
    private readonly ClientInterface $httpClient,
    private readonly string $apiKey,
    private readonly string $fromAddress,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
  }

  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
    $config = $container->get('config.factory')->get('volanea_drupal.settings');

    return new self(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('entity_type.manager')->getStorage('webform_submission'),
      $container->get('http_client'),
      (string) $config->get('api_key'),
      (string) $config->get('from_address'),
    );
  }

  public function processItem($data): void {
    $submission = $this->submissionStorage->load((int) $data['submission_id']);
    if (!$submission || $submission->getWebform()->id() !== 'contact_request') {
      return;
    }

    $values = $submission->getData();
    $email = trim((string) ($values['email'] ?? ''));
    $firstName = trim((string) ($values['first_name'] ?? 'there'));
    $topic = trim((string) ($values['topic'] ?? 'your request'));

    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
      \Drupal::logger('volanea_drupal')->warning(
        'Submission @id was not sent because its email value is invalid.',
        ['@id' => $submission->id()]
      );
      return;
    }

    if ($this->apiKey === '' || $this->fromAddress === '') {
      throw new \RuntimeException('Volanea API key or sender address is not configured.');
    }

    // Escape visitor values before interpolation into an HTML email.
    $safeName = Html::escape($firstName);
    $safeTopic = Html::escape($topic);

    $payload = [
      'from' => $this->fromAddress,
      'to' => [$email],
      'subject' => "We received your {$topic}",
      'html' => "<p>Hi {$safeName},</p><p>Thanks for contacting us about <strong>{$safeTopic}</strong>. We received your message and will reply soon.</p>",
      'text' => "Hi {$firstName},\n\nThanks for contacting us about {$topic}. We received your message and will reply soon.",
      'tags' => ['drupal', 'contact-request'],
    ];

    try {
      $response = $this->httpClient->request('POST', 'https://api.volanea.com/v1/send', [
        'headers' => [
          'Authorization' => 'Bearer ' . $this->apiKey,
          'Accept' => 'application/json',
          'Idempotency-Key' => 'drupal-contact-request-' . $submission->id(),
        ],
        'json' => $payload,
        'connect_timeout' => 5,
        'timeout' => 15,
      ]);

      if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
        throw new \RuntimeException('Volanea returned HTTP ' . $response->getStatusCode());
      }
    }
    catch (GuzzleException $e) {
      // Throwing causes Drupal's queue system to retain this item for retry.
      throw new \RuntimeException('Volanea request failed: ' . $e->getMessage(), 0, $e);
    }
  }

}

The field mapping is explicit:

Drupal Webform valueVolanea fieldPurpose
emailto[0]Recipient address for the acknowledgement
first_namehtml and textPersonal greeting
topicsubject, html, and textHuman-readable reason for the request
fixed module configfromAuthenticated sender identity
submission IDIdempotency-Key headerStable identity for retries

Do not include the visitor’s message field in the acknowledgement unless that is genuinely useful. Echoing sensitive form content back into inboxes can create unnecessary copies of private information. If your internal team needs the full submission, send a separate staff notification to a controlled address or link authorized staff to Drupal’s protected submission view.

Store the Volanea API key outside exported config

The worker reads api_key and from_address from the Drupal configuration object named volanea_drupal.settings. The sender address can safely be ordinary managed configuration. The API key should not be committed to Git, exported into shared configuration files, exposed through a Drupal settings form, or placed in any front-end drupalSettings object.

Use a configuration override in settings.php, populated from the hosting environment’s secret store:

// web/sites/default/settings.php
$config['volanea_drupal.settings']['api_key'] = getenv('VOLANEA_API_KEY');
$config['volanea_drupal.settings']['from_address'] = getenv('VOLANEA_FROM_ADDRESS');

Then create a non-secret default configuration file at config/install/volanea_drupal.settings.yml:

api_key: ''
from_address: ''

In production, define VOLANEA_API_KEY and VOLANEA_FROM_ADDRESS in the host, container, platform secret manager, or deployment environment. Drupal reads them on the server when the queue worker runs. A browser never receives them.

Why this matters goes beyond accidental source-control exposure. Drupal configuration is commonly exported between environments, inspected by administrators, included in deployment artifacts, and copied into backups. A transactional email key has real side effects: anyone holding it may be able to create sends under your authenticated sender identity. Treat it like a password with operational impact, not like a normal feature toggle.

Use a restricted key if your Volanea account supports scoped credentials. Rotate it when a team member leaves, if a deployment artifact may have leaked, or as part of your credential-management schedule. Log response status and submission IDs, but never log the Authorization header or the full key.

Test the complete path before enabling the public form

A reliable test checks more than whether the API returns a success status. Test the real Drupal event, queue processing, message content, and inbox outcome.

Start with this sequence:

  1. Authenticate the sender domain in Volanea and set VOLANEA_FROM_ADDRESS to a sender on that domain.
  2. Install and enable the custom module on a non-production Drupal environment.
  3. Submit the contact_request Webform using an inbox you control.
  4. Confirm that the submission appears in Drupal’s Webform results.
  5. Run cron or the queue worker using your normal site process.
  6. Confirm the acknowledgement arrives and that the recipient, subject, HTML, and text are correct.
  7. Submit a form with a missing or invalid email value to verify that it is logged as a permanent data issue rather than retried indefinitely.
  8. Temporarily use an invalid API key in a private test environment to ensure the queue item fails visibly and the form confirmation remains unaffected.

The API accepting a request is not the same thing as a recipient seeing the message. Inbox placement can still be affected by domain authentication, recipient suppression, mailbox filtering, and the recipient’s own mail system. For that reason, test a real inbox and not only an HTTP response.

When you move from an acknowledgement to several notification types, use distinct tags such as drupal-contact-request, drupal-application-received, and drupal-passwordless-link. Tags make it easier to recognize a message class during operational review without putting sensitive form data into logs.

When this breaks: failures specific to the Drupal-to-Volanea hop

Every outbound email integration has an ambiguous state: Drupal can lose the HTTP response after Volanea has already accepted the message. If the queue worker retries without a stable idempotency value, the recipient can receive duplicate acknowledgements.

Drupal retries can create duplicates

Drupal queues retry an item when the worker throws an exception. That is useful for a temporary DNS error, connection failure, or HTTP timeout. But it is dangerous if each attempt gets a new request identity.

This guide uses drupal-contact-request-{submission_id} as the Idempotency-Key. Keep that same value for every retry of that one acknowledgement. Do not generate a fresh random key inside processItem() on every attempt. A new key tells the API that the retry is new work and can result in a second message.

Also distinguish an updated submission from a new submission. The code reacts only to the insert hook, so an editor changing a field later will not automatically send another acknowledgement. If your process requires a new email on a status change, give that event its own durable business identifier, such as application-123-approved, rather than reusing the initial submission ID.

Webhook or HTTP timeouts are not proof of failure

This integration is an outbound HTTP call from Drupal, not a browser webhook. Still, it has the same timeout ambiguity: a 15-second client timeout means Drupal stopped waiting, not necessarily that Volanea did nothing. The request may have reached Volanea and the response may have been lost on the return path.

That is exactly why the stable idempotency key belongs on the send request. Use conservative connection and overall timeouts so a dead network does not block cron workers forever, but do not “fix” a timeout by treating it as definitively unsent. Retry with the same key.

For critical messages, record a local send ledger as an additional safeguard. A custom database table can store the submission ID, message type, idempotency key, first attempt time, completion time, and returned provider message identifier. Add a uniqueness constraint on (submission_id, message_type). This gives your team a durable way to investigate an email without guessing from queue logs alone.

Payload fields may be missing, renamed, or differently shaped

Webform element machine names are site-specific. One environment may have email; another may use contact_email. A field can be removed during a form redesign, an optional element can be empty, and select values can differ from their display labels. That is why the worker reads with defaults, validates the email address, and only maps fields it expects.

Do not assume every plan, module, or form configuration supplies the same optional fields. In particular, a form can omit first_name or topic, and a Webform might be submitted programmatically with different data. Keep the message useful when optional values are blank, and fail permanently when the destination email is invalid.

Volanea’s REST contract can also evolve. Check the current sending documentation before adding optional message features such as templates, attachments, scheduling, or reply-to behavior. Do not invent fields based on another email provider’s API; a request that looks familiar may be rejected or behave differently.

A permanent API error should not loop forever

Not every error deserves a retry. A malformed email address, missing sender configuration, invalid API key, or rejected request payload usually needs human correction. A connection reset, temporary 5xx response, or transient DNS failure may be retryable.

The compact example throws for Guzzle transport errors so Drupal retains the queue item. In a production module, add deliberate response handling: log the HTTP status and a redacted response body, retry only documented transient conditions, and mark permanent failures as completed after recording an actionable error. Otherwise, a bad form field or revoked key can occupy the queue on every cron run.

Cron scheduling affects delivery time

Drupal’s queue worker runs under cron in the example. If cron runs every 15 minutes, an acknowledgement may wait nearly 15 minutes even when everything is healthy. For low-volume contact forms that may be acceptable. For password resets, payment receipts, or login links, use a more frequent worker process or a dedicated queue runner and design monitoring around queue age.

The email API is only one part of the latency budget. A fast Volanea response cannot compensate for a Drupal cron process that has not run. Measure from Webform submission timestamp to API acceptance and alert on growing queue depth or unusually old queue items.

Operational improvements after the first working version

Once the basic acknowledgement works, improve it in ways that preserve the same architecture.

Use a template when the message becomes a product asset

Inline html and text are ideal for proving the integration. As the email gains branding, legal content, locale variants, or reusable components, a provider-managed template can reduce duplicated markup. Keep the Drupal mapping layer, but send template data rather than dynamically concatenating a large HTML document in PHP.

The key rule is unchanged: only interpolate data you trust or have escaped, and only put information in the email that the recipient should receive. A template does not make personal-data handling automatic.

Separate customer and staff sends

A confirmation to the person who submitted the form is a different message from an internal notification to a sales or support team. Give them separate queue types or a message_type in the queue item, separate tags, and separate idempotency keys. For example:

  • drupal-contact-request-847-customer-confirmation
  • drupal-contact-request-847-staff-alert

That keeps retries independent. A staff alert failing should not cause a customer acknowledgement to repeat, and vice versa.

Validate earlier, but validate again in the worker

Webform’s email element should validate the address before allowing normal submission. Keep that user-facing validation. Still validate in the queue worker because background processing must not rely on assumptions about how a submission was created. Imports, migrations, admin edits, and custom code can bypass paths that a public form uses.

If your form accepts addresses copied from another system, consider verifying them before adding them to a long-lived contact workflow. Volanea provides a free address verification tool that can help investigate questionable addresses, but do not use verification as a substitute for user consent or a clear transactional purpose.

Monitor outcomes without over-collecting data

Log the submission ID, webform ID, message type, HTTP status category, and idempotency key. Avoid logging the full form message or recipient address unless your privacy and retention requirements explicitly justify it. If you need an identifier for debugging, store a one-way hash of the email address or use Drupal’s protected submission record as the source of truth.

This produces better incident response. You can answer “Did submission 847 create an email request?” without spreading private message content across application logs, external log aggregators, and developer laptops.

Alternatives and when to choose them

A custom module is the best direct route when you control Drupal code and need reliable, low-latency transactional messages. It is not the only possible path.

The Webform Remote Handlers contributed module can send Webform results to third-party REST or SOAP endpoints and supports configurable endpoint details and JSON payloads with tokens. That can work for a simple outbound integration. However, you must confirm that its authentication options, secret-handling model, retry behavior, and payload controls match the Volanea API requirements before using it for production mail. A generic remote handler should not be assumed to support every HTTP header or idempotency policy you need.

Drupal’s ECA ecosystem can model visual event-condition-action workflows. It can be useful where non-developers need to maintain workflow logic. But the HTTP action still needs secure secret storage, exact payload mapping, and a retry policy. Visual automation changes who edits the flow; it does not remove the need for reliable integration design.

Middleware can also make sense when your Drupal host cannot run custom code or outbound HTTP is constrained. In that case, Drupal can send a signed event to a server-side middleware service, and the middleware can call Volanea with the API key held in its own secret store. Do not use a client-side Zapier, Make, or browser automation step that requires the Volanea secret to be available to site visitors.

For most developer-managed Drupal deployments, the direct queue-worker route is simpler: fewer vendors, fewer transformations, one durable source record, and one clearly owned retry boundary.

Conclusion

To send email from Drupal with Volanea, start with a real Drupal event: a newly created, completed Webform submission. Save the submission first, queue the email work, map machine-name fields explicitly, and make POST /v1/send from Drupal’s server-side HTTP client.

The practical safeguards are as important as the endpoint call. Keep the Volanea API key in server-side environment-backed configuration, escape user values before HTML interpolation, include a plain-text alternative, and reuse one idempotency key for every retry of the same logical message. That combination turns a basic form acknowledgement into an integration that can survive slow networks, queue retries, and ordinary form changes.

FAQ

Does Volanea have a Drupal module or marketplace plugin?

No native Volanea Drupal module or marketplace installation flow is required for this approach. The integration uses a small custom Drupal module, Webform’s submission hooks, Drupal’s core HTTP client, and Volanea’s REST API.

What Drupal event starts the email send?

In this guide, the trigger is a newly inserted, completed webform_submission entity from the Webform whose machine name is contact_request. The module queues work from hook_webform_submission_insert().

Where should the Volanea API key live in Drupal?

Store it outside exported Drupal configuration, ideally as a hosting-platform secret exposed through an environment variable and applied as a server-side settings.php configuration override. Never put it in JavaScript, a public Webform setting, or client-visible Drupal configuration.

How do I prevent duplicate acknowledgement emails?

Use an Idempotency-Key tied to the business event, such as the Webform submission ID plus the message type, and reuse the exact same key on every retry. Do not generate a fresh key each time the queue worker runs.

Why use a queue instead of calling Volanea during form submission?

A queue keeps a slow or temporarily failed email API request from delaying or breaking the visitor’s Webform confirmation. It also gives Drupal a controlled retry mechanism while preserving a stable idempotency key for the email send.