graystackit / laravel-ahasend-api
Laravel package for the Ahasend email API, powered by Saloon v4
Package info
github.com/GraystackIT/laravel-ahasend-api
pkg:composer/graystackit/laravel-ahasend-api
Requires
- php: ^8.2
- illuminate/database: ^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^10.0|^11.0|^12.0|^13.0
- illuminate/mail: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
- saloonphp/saloon: ^4.0
- symfony/mailer: ^6.4|^7.0
- symfony/mime: ^6.4|^7.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
- saloonphp/saloon: ^4.0
This package is auto-updated.
Last update: 2026-05-05 18:40:46 UTC
README
A production-ready Laravel package for the Ahasend transactional email API, powered by Saloon v4.
Requirements
- PHP 8.3+
- Laravel 11, 12, or 13
- Saloon 4.x
- Symfony Mailer 6.4 / 7.x (bundled with Laravel)
Installation
composer require graystackit/laravel-ahasend-api
The service provider is auto-discovered via Laravel's package discovery.
Publish config
php artisan vendor:publish --tag=ahasend-config
Publish & run migrations (optional — only needed for database storage driver)
php artisan vendor:publish --tag=ahasend-migrations php artisan migrate
Configuration
Set the following variables in your .env file:
AHASEND_API_KEY=your-api-key AHASEND_ACCOUNT_ID=your-account-id AHASEND_FROM_ADDRESS=hello@yourdomain.com AHASEND_FROM_NAME="Your App" # Optional AHASEND_BASE_URL=https://api.ahasend.com/v2 AHASEND_WEBHOOK_SECRET=your-webhook-secret AHASEND_STORE_LOGS=true AHASEND_STORAGE_DRIVER=database # "log" or "database" AHASEND_RETRY_TIMES=3 AHASEND_RETRY_DELAY_MS=500
Note:
AHASEND_ACCOUNT_IDis required. You can find your account ID in the Ahasend dashboard.
Usage
Dependency injection
use GraystackIT\Ahasend\AhasendService; class OrderController { public function __construct(private readonly AhasendService $mailer) {} public function confirm(): void { $this->mailer->sendHtml( to: [['email' => 'customer@example.com', 'name' => 'Jane']], subject: 'Order confirmed', htmlContent: '<p>Your order is confirmed!</p>', textContent: 'Your order is confirmed!', ); } }
Plain-text email
$mailer->sendText( to: [['email' => 'user@example.com']], subject: 'Hello', textContent: 'Hello from Ahasend!', );
HTML email
$mailer->sendHtml( to: [['email' => 'user@example.com']], subject: 'Hello', htmlContent: '<h1>Hello!</h1>', textContent: 'Hello!', // optional plain-text fallback );
Email with attachments
$mailer->sendWithAttachments( to: [['email' => 'user@example.com']], subject: 'Your invoice', attachments: [ ['path' => storage_path('invoices/inv-001.pdf')], // file path ['name' => 'data.csv', 'content' => $csvBase64, 'mime_type' => 'text/csv'], // raw ], htmlContent: '<p>Please find your invoice attached.</p>', );
CC / BCC
Pass cc and bcc arrays to any convenience method:
$mailer->sendHtml( to: [['email' => 'a@example.com']], subject: 'Test', htmlContent: '<p>Hi</p>', cc: [['email' => 'b@example.com']], bcc: [['email' => 'c@example.com']], );
Low-level EmailMessage
use GraystackIT\Ahasend\Data\EmailMessage; $message = new EmailMessage( fromEmail: 'from@example.com', fromName: 'Sender', to: [['email' => 'to@example.com']], subject: 'Custom', htmlContent: '<p>Hello</p>', ); $ahasendMessageId = $mailer->send($message);
Webhook handling
Register your endpoint URL in the Ahasend dashboard:
https://yourdomain.com/ahasend/webhook
The path is configurable via AHASEND_WEBHOOK_PATH. Incoming events fire Laravel events you can listen to:
| Ahasend event | Laravel event |
|---|---|
delivered |
MailDelivered |
opened |
MailOpened |
failed |
MailFailed |
bounced |
MailBounced |
Listening to events
// In EventServiceProvider or a listener class: Event::listen(MailDelivered::class, function (MailDelivered $event): void { // $event->messageId, $event->recipient, $event->payload });
Laravel Mail driver
The package registers a native Laravel mail transport driver so you can send any standard Laravel Mailable through AhaSend without touching your existing Mailable code.
1. Configure the mailer
Add an ahasend entry to the mailers array in config/mail.php:
// config/mail.php 'mailers' => [ // ... other mailers ... 'ahasend' => [ 'transport' => 'ahasend', ], ],
The transport reads API credentials and sender defaults from the ahasend config (i.e. the same AHASEND_* variables you already set).
To make AhaSend the default mailer, update your .env:
MAIL_MAILER=ahasend
2. Send a Mailable
use App\Mail\OrderShipped; use Illuminate\Support\Facades\Mail; // Uses the default mailer if MAIL_MAILER=ahasend Mail::to('customer@example.com')->send(new OrderShipped($order)); // Or target the driver explicitly Mail::mailer('ahasend') ->to('customer@example.com') ->cc('manager@example.com') ->send(new OrderShipped($order));
3. Example Mailable
<?php namespace App\Mail; use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailables\Content; use Illuminate\Mail\Mailables\Envelope; class OrderShipped extends Mailable { public function __construct(public readonly Order $order) {} public function envelope(): Envelope { return new Envelope(subject: 'Your order has shipped'); } public function content(): Content { return new Content( html: 'emails.order-shipped', // resources/views/emails/order-shipped.blade.php text: 'emails.order-shipped-text', ); } public function attachments(): array { return [ Attachment::fromPath(storage_path("invoices/{$this->order->id}.pdf")) ->as('invoice.pdf') ->withMime('application/pdf'), ]; } }
4. Required .env variables
AHASEND_API_KEY=your-api-key AHASEND_ACCOUNT_ID=your-account-id AHASEND_FROM_ADDRESS=hello@yourdomain.com AHASEND_FROM_NAME="Your App" # Make AhaSend the default mailer MAIL_MAILER=ahasend
Supported features
| Feature | Supported |
|---|---|
| HTML body | Yes |
| Plain-text body | Yes |
Multiple To recipients |
Yes |
| CC | Yes |
| BCC | Yes |
| File attachments | Yes (auto base64 encoded) |
| From address / name | Yes (from Mailable or config fallback) |
Transport internals
The driver is implemented as GraystackIT\Ahasend\Mail\AhaSendTransport, which extends Symfony's AbstractTransport. It converts the Symfony Email object into the EmailMessage DTO used by AhasendService::send(), preserving all recipients, headers, and attachments. Errors thrown by AhasendService are re-wrapped as Symfony TransportException so Laravel's mail system handles them consistently.
Mailable tracking
Use the TracksAhasendMail trait in any Mailable to attach a UUID X-Ahasend-Message-Id header and (optionally) store the outgoing record in the database:
use GraystackIT\Ahasend\Traits\TracksAhasendMail; use Illuminate\Mail\Mailable; class OrderShipped extends Mailable { use TracksAhasendMail; public function build(): self { $this->initAhasendTracking(recipient: $this->order->email); return $this->subject('Your order has shipped') ->view('emails.order-shipped'); } }
Messages
Retrieve and manage sent or scheduled messages via MessageService.
use GraystackIT\Ahasend\Services\MessageService; use GraystackIT\Ahasend\Enums\MessageStatus; class MyController { public function __construct(private readonly MessageService $messages) {} }
Get a single message
$message = $messages->get('msg-abc123'); echo $message->id; // 'msg-abc123' echo $message->subject; // 'Hello World' echo $message->status->value; // 'delivered' echo $message->status->isTerminal(); // true
List messages
Uses cursor-based pagination:
$result = $messages->list( limit: 25, // optional — max results to return after: 'cursor-xyz', // optional — cursor for the next page before: 'cursor-abc', // optional — cursor for the previous page ); foreach ($result['data'] as $message) { echo $message->id . ': ' . $message->subject; } // $result['meta'] contains cursor pagination info
Cancel a scheduled message
$cancelled = $messages->cancel('msg-scheduled-001'); // true on success
SMTP Credentials
Manage programmatic SMTP credentials via SmtpCredentialService.
use GraystackIT\Ahasend\Services\SmtpCredentialService; class MyController { public function __construct(private readonly SmtpCredentialService $smtp) {} }
Create an SMTP credential
// Global credential (can send from any domain) $credential = $smtp->create('My Application'); // Scoped credential (restricted to specific domains) $credential = $smtp->create( name: 'My Application', scope: 'scoped', domains: ['yourdomain.com', 'anotherdomain.com'], ); // Sandbox credential (no real emails sent) $credential = $smtp->create('Test App', sandbox: true); // Save the password — the API will not return it again. echo $credential->id; // 'cred-xyz' echo $credential->username; // 'smtp_my_application' echo $credential->password; // 'generated-secret' (only available on create) echo $credential->host; // 'smtp.ahasend.com' echo $credential->port; // 587
List all SMTP credentials
Uses cursor-based pagination:
$credentials = $smtp->list( limit: 10, // optional after: 'cursor-xyz', // optional before: 'cursor-abc', // optional ); foreach ($credentials as $cred) { echo $cred->id . ': ' . $cred->name; }
Get a single SMTP credential
$credential = $smtp->get('cred-xyz');
Delete an SMTP credential
$smtp->delete('cred-xyz'); // true on success
Suppressions
Manage the suppression list via SuppressionService.
use GraystackIT\Ahasend\Services\SuppressionService; class MyController { public function __construct(private readonly SuppressionService $suppressions) {} }
Add a suppression
$suppression = $suppressions->create( email: 'user@example.com', expiresAt: '2026-12-31T00:00:00Z', // RFC3339 datetime — required reason: 'User unknown', // optional domain: 'example.com', // optional — restrict to a sending domain ); echo $suppression->email; // 'user@example.com'
List suppressions
Uses cursor-based pagination:
$result = $suppressions->list( limit: 50, // optional after: 'cursor-xyz', // optional before: 'cursor-abc', // optional domain: 'example.com', // optional — filter by sending domain email: 'user@example.com', // optional — filter by recipient email ); foreach ($result['data'] as $suppression) { echo $suppression->email; } // $result['meta'] contains cursor pagination info
Delete a specific suppression
$suppressions->delete('user@example.com'); // true on success
Delete all suppressions
$suppressions->deleteAll(); // true on success
Reports
Retrieve analytics data via ReportService.
use GraystackIT\Ahasend\Services\ReportService; class MyController { public function __construct(private readonly ReportService $reports) {} }
All date/time parameters use RFC3339 format (e.g. 2024-01-01T00:00:00Z).
Bounce statistics
$stats = $reports->bounceStatistics( fromTime: '2024-01-01T00:00:00Z', // optional toTime: '2024-01-31T23:59:59Z', // optional senderDomain: 'gmail.com', // optional — filter by sending domain ); echo $stats->totalSent; // 1000 echo $stats->hardBounces; // 50 echo $stats->softBounces; // 20 echo $stats->hardBounceRate; // 5.0 (percent) echo $stats->totalBounceRate; // 7.0
Deliverability breakdown
$breakdown = $reports->deliverabilityBreakdown( fromTime: '2024-01-01T00:00:00Z', // optional toTime: '2024-01-31T23:59:59Z', // optional senderDomain: 'yourdomain.com', // optional recipientDomains: 'gmail.com,outlook.com', // optional — comma-separated tags: 'transactional,welcome', // optional — comma-separated groupBy: 'day', // optional — hour, day, week, month ); echo $breakdown->totalSent; // 500 echo $breakdown->totalDelivered; // 480 echo $breakdown->deliveryRate; // 96.0 foreach ($breakdown->domains as $domain) { echo $domain['domain'] . ': ' . $domain['rate'] . '%'; }
Delivery time analytics
$analytics = $reports->deliveryTimeAnalytics( fromTime: '2024-01-01T00:00:00Z', // optional toTime: '2024-01-31T23:59:59Z', // optional senderDomain: 'yahoo.com', // optional ); echo $analytics->averageDeliverySeconds; // 45.7 echo $analytics->medianDeliverySeconds; // 30.0 echo $analytics->totalDelivered; // 900 // Breakdown by hour-of-day and calendar day foreach ($analytics->byHour as $hour) { echo "Hour {$hour['hour']}: {$hour['avg_delivery_seconds']}s avg"; }
Error handling
All service methods throw AhasendException on API errors. The exception wraps the Saloon RequestException and exposes the HTTP status code.
use GraystackIT\Ahasend\Exceptions\AhasendException; try { $messages->get('nonexistent-id'); } catch (AhasendException $e) { echo $e->getCode(); // 404 echo $e->getMessage(); // "Ahasend API error [404]: ..." }
Testing
composer test
License
MIT