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.
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.
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.
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.
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.
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.
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"
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.
<?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');
}
}
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'));
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.
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.
composer require usenotix/notix-php
NOTIX_API_KEY=notix_xxxxxxxxxxxx
NOTIX_WEBHOOK_SECRET=whsec_xxxxxxxxxxxx
// config/services.php
'notix' => [
'key' => env('NOTIX_API_KEY'),
'webhook_secret' => env('NOTIX_WEBHOOK_SECRET'),
],
<?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'];
}
}
<?php
namespace App\Services;
use Notix\Notix;
final class ReceiptMailer
{
// The service provider binds Notix\Notix, so constructor injection works.
public function __construct(private readonly Notix $notix)
{
}
public function send(string $orderId, string $to): string
{
$email = $this->notix->emails->send([
'to' => $to,
'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");
return $email['emailId'];
}
}
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.
<?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']);
// })
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.
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.
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.
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.
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.
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.
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.
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.
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.
Point any framework's mailer at one host and port; the relay turns SMTP into the same tracked, suppressed send.
GuidesThe PHP SDK, plain cURL, or PHPMailer pointed at the relay, with the same idempotency key on every retry.
Use casesSend one-time codes by email or SMS with two API calls, with expiry, length and risk scoring handled for you.
LearnWhat a pre-send deliverability check looks at, the verdict and score it returns, and the four findings that block a send.
ProductOne JSON API for transactional and marketing email: send, batch, schedule, webhooks, typed SDKs.
NotixA free plan with no card, and Pro at $15 a month for 50,000 emails. Only sent volume is metered.
Verify a domain, copy an API key, change four lines of .env. The free plan does not ask for a card.