Notix
Guides

Send Laravel mail through Notix in two ways: SMTP or the SDK.

By the end of this guide your Laravel app will send its existing Mailables through the SMTP relay with four lines of configuration, and send the messages that matter most, receipts, resets, codes, through the email API with an idempotency key and an id back. A verified webhook controller closes the loop with delivery status.

What you need

Three things, none of them a card.

An API key.

Created in the dashboard under API keys, shown once. It is both the SMTP password and the SDK credential. A sending access key is enough for this guide.

A verified domain.

Add your domain, publish the SPF, DKIM and DMARC records Notix gives you, and any from address on it becomes valid. The quickstart walks through it.

Laravel 11 or 12 on PHP 8.1+.

The SMTP path needs nothing installed. The SDK needs PHP 8.1 or newer with the curl and json extensions, and one Composer install.

The free plan covers 5,000 emails a month and 200 a day, and the free plan does not ask for a card.

Steps

From an untouched install to a queued receipt.

  1. Option A: point the smtp mailer at the relay.

    Laravel’s default smtp mailer in config/mail.php reads its host, port, credentials and encryption from .env. Set them to the relay: host smtp.usenotix.dev, port 465 with implicit TLS (or 587 with STARTTLS), username notix, and your API key as the password. MAIL_FROM_ADDRESS must be on your verified domain. Nothing in config/mail.php itself needs to change.

    .env
    MAIL_MAILER=smtp
    MAIL_HOST=smtp.usenotix.dev
    MAIL_PORT=465
    MAIL_USERNAME=notix
    # The SMTP password is a Notix API key.
    MAIL_PASSWORD=notix_xxxxxxxxxxxx
    # 465 is implicit TLS. For STARTTLS use MAIL_PORT=587 and MAIL_ENCRYPTION=tls.
    MAIL_ENCRYPTION=ssl
    MAIL_FROM_ADDRESS=receipts@acme.com
    MAIL_FROM_NAME="Acme"
    
  2. Write a Mailable and send it.

    A Mailable is unchanged by the relay: subject in envelope(), a Blade or Markdown view in content(). Mail::to() sends it through whichever mailer MAIL_MAILER names, which is now the relay. The relay turns the SMTP session into the same tracked, suppressed send the API makes.

    app/Mail/OrderReceipt.php
    <?php
    
    namespace App\Mail;
    
    use Illuminate\Bus\Queueable;
    use Illuminate\Mail\Mailable;
    use Illuminate\Mail\Mailables\Content;
    use Illuminate\Mail\Mailables\Envelope;
    use Illuminate\Queue\SerializesModels;
    
    class OrderReceipt extends Mailable
    {
        use Queueable, SerializesModels;
    
        public function __construct(public readonly string $orderId)
        {
        }
    
        public function envelope(): Envelope
        {
            return new Envelope(subject: "Your receipt for order {$this->orderId}");
        }
    
        public function content(): Content
        {
            return new Content(markdown: 'mail.orders.receipt');
        }
    }
    
    anywhere in your app
    use App\Mail\OrderReceipt;
    use Illuminate\Support\Facades\Mail;
    
    // Sends through the "smtp" mailer, which is the Notix relay.
    Mail::to('customer@example.com')->send(new OrderReceipt('4471'));
    
    // Or hand it to the queue and let a worker deliver it.
    Mail::to('customer@example.com')->queue(new OrderReceipt('4471'));
    
  3. Queue it.

    Mail::to()->queue() pushes the Mailable onto your queue connection and a worker delivers it through the relay. The Mailable above already uses the Queueable trait. Run php artisan queue:work in production so a slow send never holds a request open.

  4. Option B: install the SDK for the sends that must not go out twice.

    composer require usenotix/notix-php. Laravel discovers the service provider and the Notix facade automatically. Put the key in .env and add the service to config/services.php; the provider reads services.notix.key first and falls back to the NOTIX_API_KEY environment variable. Then call Notix::emails()->send() through the facade, or inject Notix\Notix from the container. The second argument is the idempotency key; the return value is the decoded JSON body, so $email['emailId'] is the id to store.

    terminal
    composer require usenotix/notix-php
    
    .env
    NOTIX_API_KEY=notix_xxxxxxxxxxxx
    NOTIX_WEBHOOK_SECRET=whsec_xxxxxxxxxxxx
    
    config/services.php
    // config/services.php
    'notix' => [
        'key' => env('NOTIX_API_KEY'),
        'webhook_secret' => env('NOTIX_WEBHOOK_SECRET'),
    ],
    
    app/Http/Controllers/OrderController.php
    <?php
    
    namespace App\Http\Controllers;
    
    use Notix\Laravel\Facades\Notix;
    
    final class OrderController extends Controller
    {
        public function receipt(string $orderId)
        {
            // The second argument is the idempotency key: a retry with the same
            // key and body returns the original message instead of sending twice.
            $email = Notix::emails()->send([
                'to' => 'customer@example.com',
                'from' => 'receipts@acme.com',
                'subject' => "Your receipt for order {$orderId}",
                'html' => '<p>Thanks for your order.</p>',
                'text' => 'Thanks for your order.',
            ], "order-{$orderId}-receipt");
    
            // Keep the id next to the order: it matches every webhook event.
            return $email['emailId'];
        }
    }
    
  5. Receive delivery status on a webhook.

    Register a webhook URL in the dashboard and copy its signing secret into NOTIX_WEBHOOK_SECRET. Notix signs every delivery: the X-Notix-Signature header is v1= plus the hex HMAC-SHA256 of timestamp.rawBody, with the timestamp in X-Notix-Timestamp. The SDK’s Webhooks class verifies both and rejects anything older than 300 seconds. Use the raw request body, and exclude the route from CSRF verification.

    app/Http/Controllers/NotixWebhookController.php
    <?php
    
    namespace App\Http\Controllers;
    
    use Illuminate\Http\Request;
    use Illuminate\Http\Response;
    use Notix\Exception\SignatureVerificationException;
    use Notix\Webhooks;
    
    final class NotixWebhookController extends Controller
    {
        public function __invoke(Request $request): Response
        {
            $webhooks = new Webhooks(config('services.notix.webhook_secret'));
    
            try {
                // Always the raw body: a re-encoded body no longer matches the signature.
                $event = $webhooks->constructEvent(
                    $request->getContent(),
                    $request->headers->all(),
                );
            } catch (SignatureVerificationException $error) {
                return response('Invalid signature', 400);
            }
    
            if ($event['type'] === 'email.delivered') {
                // $event['data']['id'] is the emailId you were given at send time.
            }
    
            return response('ok');
        }
    }
    
    // routes/web.php
    // Route::post('/notix/webhook', NotixWebhookController::class);
    
    // bootstrap/app.php: Notix is not a browser, so skip CSRF for this route.
    // ->withMiddleware(function (Middleware $middleware) {
    //     $middleware->validateCsrfTokens(except: ['notix/webhook']);
    // })
    
In production

What changes between a working send and a system you can leave alone.

Laravel gives you queues and environment files; Notix gives you an idempotency key, one exception per error code, and events. Wire the four together once.

A verified from, per environment.

MAIL_FROM_ADDRESS and every SDK from must sit on a verified domain, or the send is refused. Keep a separate key and a test domain in your staging .env, and never commit either file.

Idempotency keys on SDK sends.

Pass the key as the second argument to send, create or batch. Derive it from your own record, the order, the reset token, the invoice: a queued job that runs twice then returns the original message. The same key with a different body throws NotUniqueException, which is the SDK telling you the retry is not really a retry.

One exception per error code.

The SDK throws a Notix\Exception\NotixException subclass for each API code: BadRequestException, ForbiddenException, RateLimitedException and so on, each with getErrorCode() and getBody(). RateLimitedException::getRetryAfter() returns the seconds the API asked you to wait, which maps directly onto a job release delay.

Queue workers and batches.

Send from jobs, not controllers, so a slow SMTP session or a rate limit never blocks a request. For digests and receipts in bulk, Notix::emails()->batch() sends up to 100 emails in one request, also with an idempotency key, and returns the created ids.

inside a queued job
use Notix\Exception\NotixException;
use Notix\Exception\RateLimitedException;
use Notix\Laravel\Facades\Notix;

try {
    $email = Notix::emails()->send($payload, "order-{$orderId}-receipt");
} catch (RateLimitedException $error) {
    // The API sent Retry-After; wait that long before the next attempt.
    $retryAfter = $error->getRetryAfter() ?? 60;
    // release the job back onto the queue with a delay of $retryAfter seconds
} catch (NotixException $error) {
    // Every API error is one exception class per code: BAD_REQUEST,
    // FORBIDDEN, NOT_UNIQUE (same key, different body), ...
    report($error);
}

Before a big send, the deliverability check reports on a message’s content and authentication without sending it. One-time codes are a separate API rather than a template of your own; the OTP use case shows Notix::verify() end to end.

Using the API instead of SMTP

When a Mailable is not enough.

The relay is the shortest path and covers most of an application’s mail. Three things only the API gives you: the email id at send time, which is what webhook events and Notix::emails()->get() are keyed by; the idempotency key, which makes a queue retry safe; and the batch endpoint. When a message is tied to money or to a login, send it through the facade. Everything else can stay a Mailable. The plain-PHP version of the same SDK is in the PHP guide, and the Node and Python guides use the same idempotency and webhook conventions, so a mixed stack behaves one way.

Keep Blade for the body either way.

A Mailable renders its Blade or Markdown view before the relay sees it. With the SDK you render the same view yourself, view('mail.orders.receipt', [...])->render(), and pass the string as html. Templates stored in Notix are the third option: reference the template and pass variables instead of markup, and edit the copy without a deploy.

One key per environment.

Local, staging and production each get their own API key and their own .env. Both paths read the key from the environment, so switching an environment from the relay to the SDK, or back, is a configuration change and not a code change.

FAQ

Questions, answered.

Is there a Notix mail driver for Laravel?
No custom transport, and you do not need one. Laravel's built-in smtp mailer speaks to the relay with four .env values, so every Mailable you already have keeps working unchanged. When you want the email id back, an idempotency key on retries, or batch sends, the PHP SDK's Notix facade gives you those through the JSON API instead. Most apps use both: the mailer for Mailables, the facade for the sends that must not go out twice.
Which should I pick, the SMTP mailer or the SDK?
Start with the SMTP mailer if your app already sends Mailables: it is a configuration change, and suppression, tracking and webhooks apply to relayed mail exactly as they do to API sends. Reach for the SDK where the send is tied to money or security, a receipt, a password reset, a code, because only the API takes an idempotency key and returns the id you match webhook events against.
Can I queue Notix mail?
Yes, both ways. Mail::to()->queue() hands a Mailable to your queue worker, which then talks to the relay. With the SDK, wrap the facade call in a queued job; give the job the idempotency key derived from your own record so a retried job returns the original message rather than sending a second one.
Why does my from address get rejected?
The from address must be on a domain you have verified in Notix, with the SPF, DKIM and DMARC records it gives you published. MAIL_FROM_ADDRESS in .env, or the from field on an SDK send, has to use that domain; an unverified one answers BAD_REQUEST on the API and a rejection on the relay.
How do I verify a webhook in Laravel?
Register a POST route to a controller, build Notix\Webhooks with the secret from config('services.notix.webhook_secret'), and pass the raw request body and the headers to constructEvent. It checks the X-Notix-Signature HMAC over timestamp.rawBody, rejects anything older than 300 seconds, and returns the decoded event. Exclude the route from CSRF verification, because Notix is not a browser session.

Send the first one now.

Verify a domain, copy an API key, change four lines of .env. The free plan does not ask for a card.