> ## Documentation Index
> Fetch the complete documentation index at: https://ahasend.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> AhaSend stores all message data in the EU. See /facts.
> The API base URL is https://api.ahasend.com. Send the API key in the Authorization: Bearer header. See /api-reference/authentication.
> Use send.ahasend.com for SMTP. Ports 25, 587 and 2525 use STARTTLS. Port 465 is not supported. See /smtp.
> For send-only integrations, create an API Key v2 under Credentials → Add and scope it to messages:send:{your-domain}. See /send-api/credentials and /api-reference/scopes.

# Send Drupal Email with the AhaSend Module

> Send Drupal email through AhaSend API v2 with the AhaSend module: add a Symfony Mailer transport, test in sandbox mode and log events from signed webhooks.

The [AhaSend module](https://www.drupal.org/project/ahasend) adds an AhaSend transport to Drupal's Symfony Mailer module. Your site's email, such as password resets, account notifications and form submissions, is sent through the AhaSend API v2. The module can also receive signed AhaSend webhooks and show the delivery events of each message in Drupal.

## Prerequisites

* Drupal 10.6 or later, or Drupal 11, with PHP 8.1 or later, Composer and Drush.
* A [verified sending domain](/docs/domains.md).
* An API v2 key.

Composer also installs the required [Symfony Mailer](https://www.drupal.org/project/symfony_mailer) module. The AhaSend module works with Symfony Mailer 1.6 or later and with 2.x, which is called Mailer Plus. This guide notes where the two differ.

> [!NOTE]
>
> Version 2.x of the module uses API v2. It replaces version 1.x, which used the legacy API v1 and the Mail System module. Version 1.x is no longer supported and has no upgrade path.

## Install the Module

From your Drupal project root:

```bash theme={null}
composer require drupal/ahasend:^2.0
drush en ahasend -y
```

Enabling the module also enables Symfony Mailer.

With Mailer Plus 2.x, Drupal core emails go through Mailer Plus only when an override is enabled for them. Enable the Mailer Override submodule:

```bash theme={null}
drush en mailer_override -y
```

Then open `/admin/config/system/mailer/override` and enable the overrides for the emails your site sends, for example **User** for account and password-reset emails.

## Add the AhaSend Transport

1. At **Configuration → System → Basic site settings**, set the site's **Email address** to an address on your verified domain. Drupal sends from this address by default.

2. Open `/admin/config/system/mailer/transport`, choose **AhaSend (API v2)** as the **Transport type** and click **Add transport**.

3. Enter a label and fill in the AhaSend fields:

   | Field | Value |
   | - | - |
   | **API key** | Your `aha-sk-` key. The saved key is never shown again; select **Change API key** to replace it. |
   | **Account ID** | Your account ID (a UUID). |
   | **Mode** | **Live**, or a simulated result: **Sandbox: deliver**, **Sandbox: bounce**, **Sandbox: defer**, **Sandbox: fail** or **Sandbox: suppress**. |
   | **Open tracking**, **Click tracking** | **Account default**, **On** or **Off**. See [open tracking](/docs/tracking/open-tracking.md) and [click tracking](/docs/tracking/click-tracking.md). |

4. Set **Mode** to **Sandbox: deliver** for your first tests. AhaSend records sandbox sends and sends their webhook events, but delivers nothing. See [sandbox mode](/docs/send-api/sandbox.md).

5. Click **Test connection**. It calls the [ping endpoint](/docs/api-reference/utility/ping.md), which checks only the API key. It does not check the account ID, the key's scope or your sender address; the first real send does.

6. Save the transport, then click **Set as default** next to it in the transport list.

With the Key module installed, the form also shows **API key source**; see [Keep Secrets Out of Configuration](#keep-secrets-out-of-configuration).

## Send a Test Email

* Symfony Mailer 1.x: open the **Test** tab at `/admin/config/system/mailer/test`.
* Mailer Plus 2.x: open `/admin/config/system/mailer`.

Enter a recipient and click **Send**. Drupal's confirmation only shows that it tried to send. Find the message in the AhaSend dashboard and in **Reports → AhaSend message log** (`/admin/reports/ahasend`). If it is missing, look for AhaSend errors in **Reports → Recent log messages**.

Then test the emails your site depends on, such as password reset, new account and form notifications.

When the tests pass, set **Mode** to **Live**. A key created in **Sandbox** mode keeps every send in sandbox, even when the transport is set to **Live**, so use a production key on the live site.

## Receive Delivery Events with Webhooks

Webhooks are optional. With them, the message log shows what happened to each email after AhaSend accepted it.

1. Open **Configuration → Web services → AhaSend** (`/admin/config/services/ahasend`) and copy the **Webhook URL**, for example `https://example.com/ahasend/webhook`.
2. In the AhaSend dashboard, open **Webhooks**, click **Add Webhook**, paste the URL, choose the events to send and create the webhook. See [webhooks](/docs/integrations/webhooks.md).
3. Copy the **Webhook Secret** from the webhook's details page, paste it into **Signing secret** in Drupal and save.
4. Send a sandbox test email. After a short delay, its entry in the message log shows the reception and delivery events.

Save the secret right after you create the webhook. Until Drupal has a secret, the webhook URL answers 404, and AhaSend disables a webhook after more than 100 consecutive failed deliveries.

The webhook URL is public. Each delivery is authenticated by its [signature](/docs/integrations/webhooks.md), and deliveries with an invalid signature get a 401 response. Deliveries whose timestamp is more than 5 minutes away from the server's time are rejected, so keep the server clock in sync. The URL stays reachable in maintenance mode. After a long outage, check in the AhaSend dashboard that the webhook is still enabled.

## Message Log

**Reports → AhaSend message log** (`/admin/reports/ahasend`) has two tabs:

* **Messages**: one entry per recipient, with the send time, subject, sender, recipient and status. The status follows the webhook events received for the message and uses the dashboard's names: **Received**, **Deferred**, **Delivered**, **Opened**, **Clicked**, **Bounced**, **Failed** or **Suppressed**. Until the first event arrives, or if no webhook is set up, it shows the result AhaSend returned when the email was sent, such as **Queued**. Open an entry to see its delivery timeline from webhooks, such as **Message Delivered**, **Message Deferred**, **Message Bounced** or **Message Opened**, with the receiving server's SMTP response when AhaSend reports one.
* **Events**: account-level events that belong to no single message, such as suppressions and DNS errors.

## Keep Secrets Out of Configuration

By default, Drupal saves the API key and the webhook signing secret in its configuration, so `drush config:export` writes them to your export files. To avoid this, use the [Key module](https://www.drupal.org/project/key) 1.18 or later:

1. Install and enable it:

   ```bash theme={null}
   composer require drupal/key
   drush en key -y
   ```

2. At `/admin/config/system/keys`, add a key of type **Authentication** that uses the **File** or **Environment** provider. A key that uses the **Configuration** provider is exported too, and the module warns you when you select one.

3. In the transport form, set **API key source** to **Use a key from the Key module** and select the key. In the AhaSend settings form, do the same with **Signing secret source**.

Only the key's name is saved. If you delete a key the module uses, Drupal warns you, and sending or webhook processing stops until you select another key.

To use different values on each environment, override them in `settings.php`. Replace `TRANSPORT_ID` with the transport's machine name:

```php settings.php theme={null}
// Symfony Mailer 1.x.
$config['symfony_mailer.mailer_transport.TRANSPORT_ID']['configuration']['api_key'] = getenv('AHASEND_API_KEY');
// Mailer Plus 2.x.
$config['mailer_transport.mailer_transport.TRANSPORT_ID']['configuration']['api_key'] = getenv('AHASEND_API_KEY');
// Webhook signing secret.
$config['ahasend.settings']['webhook_secret'] = getenv('AHASEND_WEBHOOK_SECRET');
```

An override wins over the value saved in the form, but the transport form still stores any key you enter there. A transport that uses a key from the Key module ignores an `api_key` override; override `api_key_id` to select a different key instead. Review [key security](/docs/security.md) and [IP allow lists](/docs/security/ip-allow-lists.md).

## React to Webhook Events in Code

Every verified webhook delivery dispatches the `AhaSendWebhookEvent::RECEIVED` event (`ahasend.webhook_received`). The module stores each delivery at priority 100 and stops retried deliveries there, so subscribe with a lower priority to react once per event. This subscriber in a custom module logs each bounce:

```php src/EventSubscriber/AhaSendBounceSubscriber.php theme={null}
<?php

namespace Drupal\mymodule\EventSubscriber;

use Drupal\ahasend\Event\AhaSendWebhookEvent;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

final class AhaSendBounceSubscriber implements EventSubscriberInterface {

  public function __construct(
    private readonly LoggerChannelFactoryInterface $loggerFactory,
  ) {}

  public static function getSubscribedEvents(): array {
    return [AhaSendWebhookEvent::RECEIVED => ['onWebhook', 0]];
  }

  public function onWebhook(AhaSendWebhookEvent $event): void {
    if ($event->getType() !== 'message.bounced') {
      return;
    }
    $recipient = $event->getData()['recipient'] ?? '';
    $this->loggerFactory->get('mymodule')->warning('AhaSend reported a bounce for @recipient.', [
      '@recipient' => $recipient,
    ]);
  }

}
```

Register it in `mymodule.services.yml`:

```yaml mymodule.services.yml theme={null}
services:
  mymodule.ahasend_bounce_subscriber:
    class: Drupal\mymodule\EventSubscriber\AhaSendBounceSubscriber
    arguments: ['@logger.factory']
    tags:
      - { name: event_subscriber }
```

Add `ahasend:ahasend` to the `dependencies` in `mymodule.info.yml`, so the AhaSend module cannot be uninstalled while your subscriber uses its event class.

The event also has `getPayload()`, `getMessageIdHeader()` and `getDeliveryId()`. The signature proves a delivery came from AhaSend, but values such as subjects, addresses and SMTP responses come from third parties: treat them as untrusted input. See [webhooks](/docs/integrations/webhooks.md) for the event types and their data.

## Troubleshooting

* **AhaSend (API v2) is missing from the transport types**: clear caches with `drush cr`.
* **Test connection passes, but emails fail**: check the account ID, that the key's scope covers your sender domain, and that the sender address uses a verified domain. AhaSend's error message is in **Reports → Recent log messages**; see [API errors](/docs/api-reference/errors.md).
* **Emails are accepted but never arrive**: check that **Mode** is **Live** and that the API key is not a sandbox key.
* **Core emails skip AhaSend on Mailer Plus 2.x**: enable Mailer Override and the override for those emails.
* **The webhook URL answers 404**: no signing secret is saved, or the selected key is missing or empty.
* **Webhook deliveries get 401**: the signing secret does not match the webhook, or the server clock is more than 5 minutes off.
* **Webhooks stopped arriving**: AhaSend disables a webhook after more than 100 consecutive failed deliveries. Fix the cause, then enable it again in the dashboard.

## Related Guides

* [Domain setup](/docs/domains.md), [API credentials](/docs/send-api/credentials.md), [scoped credentials](/docs/security/scoped-credentials.md) and [sandbox mode](/docs/send-api/sandbox.md).
* [Webhooks](/docs/integrations/webhooks.md), [bounces and suppressions](/docs/bounces.md), [open tracking](/docs/tracking/open-tracking.md) and [click tracking](/docs/tracking/click-tracking.md).
* [Troubleshooting](/docs/troubleshooting.md), [API errors](/docs/api-reference/errors.md) and [key security](/docs/security.md).
* The module's [project page](https://www.drupal.org/project/ahasend) and [issue queue](https://www.drupal.org/project/issues/ahasend).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.