An API key.
Created in the dashboard under API keys, shown once. A sending access key is enough for this guide and cannot read contacts or delete domains if it leaks.
By the end of this guide you will have sent a real email from a PHP script through the email API, seen the same send made with plain cURL and with PHPMailer through the SMTP relay, and added the three things a production send needs: an idempotency key, exception handling, and a webhook for delivery status. On Laravel? The Laravel guide covers the service provider and config/mail.php.
Created in the dashboard under API keys, shown once. A sending access key is enough for this guide and cannot read contacts or delete domains if it leaks.
Add your domain, publish the SPF, DKIM and DMARC records Notix gives you, and the from address on it becomes valid. The quickstart walks through it.
With the curl and json extensions. The SDK is one Composer install:
composer require usenotix/notix-php
The free plan covers 5,000 emails a month and 200 a day, and the free plan does not ask for a card.
new Notix($apiKey) is all it takes. The optional second argument is a different Notix base URL; when you omit it the client reads NOTIX_BASE_URL from the environment and falls back to https://app.usenotix.dev. Connect and total timeouts default to 10 and 30 seconds and are constructor arguments too. Read the key from the environment; an empty key throws at construction rather than on the first send.
<?php
require __DIR__ . '/vendor/autoload.php';
use Notix\Notix;
// Keep the key in the environment, never in source.
$notix = new Notix(getenv('NOTIX_API_KEY'));
// Pointing at a different Notix base URL (a staging environment, say)?
// Pass it as the second argument, or set
// NOTIX_BASE_URL in the environment. The default is https://app.usenotix.dev.
// $notix = new Notix(getenv('NOTIX_API_KEY'), 'https://notix.example.com');
$notix->emails->send takes an array with from, to, subject and html or text (send both; some clients only show one) and returns the decoded JSON body as an associative array. The cURL tab is the same call without the SDK: one POST to /api/v1/emails with a Bearer token, and the Idempotency-Key header the SDK sets for you when you pass a key.
<?php
require __DIR__ . '/vendor/autoload.php';
use Notix\Notix;
$notix = new Notix(getenv('NOTIX_API_KEY'));
$email = $notix->emails->send([
'from' => 'receipts@acme.com',
'to' => 'customer@example.com',
'subject' => 'Your receipt for order 4471',
'html' => '<p>Thanks for your order.</p>',
'text' => 'Thanks for your order.',
]);
// Every method returns the decoded JSON body as an array.
echo 'queued ' . $email['emailId'] . PHP_EOL;
<?php
$ch = curl_init('https://app.usenotix.dev/api/v1/emails');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . getenv('NOTIX_API_KEY'),
// Same key on every retry: the same message is never sent twice.
'Idempotency-Key: order-4471-receipt',
],
CURLOPT_POSTFIELDS => json_encode([
'from' => 'receipts@acme.com',
'to' => 'customer@example.com',
'subject' => 'Your receipt for order 4471',
'html' => '<p>Thanks for your order.</p>',
'text' => 'Thanks for your order.',
]),
]);
$body = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) {
// Every error is { "error": { "code", "message" } }.
echo $body['error']['code'] . ': ' . $body['error']['message'] . PHP_EOL;
} else {
echo 'queued ' . $body['emailId'] . PHP_EOL;
}
The response carries emailId. Store it next to the order, the user or whatever caused the send: it is the key you read the message back with ($notix->emails->get($id)) and the data.id every webhook event for that message carries. An API error throws a Notix\Exception\NotixException subclass named for the status; a network failure throws ConnectionException. Each API exception carries getErrorCode(), getHttpStatus() and getBody().
<?php
use Notix\Exception\NotixException;
use Notix\Exception\RateLimitedException;
use Notix\Exception\ConnectionException;
try {
$email = $notix->emails->send($payload);
} catch (RateLimitedException $error) {
// 429. Wait for the Retry-After the API sent, then try again.
sleep($error->getRetryAfter() ?? 60);
} catch (NotixException $error) {
// Any other API error: BadRequestException (400), ForbiddenException (403),
// NotUniqueException (409) and so on. All carry the API's code and body.
error_log($error->getErrorCode() . ': ' . $error->getMessage());
} catch (ConnectionException $error) {
// The request never reached Notix. Safe to retry with the same key.
error_log('network: ' . $error->getMessage());
}
Already on PHPMailer? Point it at the SMTP relay. Host smtp.usenotix.dev, port 465 with implicit TLS (or 587 for STARTTLS), username notix, and your API key as the password. The relay posts the parsed message to the same endpoint the SDK calls, so suppression, tracking and webhooks apply either way.
<?php
use PHPMailer\PHPMailer\PHPMailer;
$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host = 'smtp.usenotix.dev';
// Implicit TLS on 465. Use port 587 with ENCRYPTION_STARTTLS instead if you prefer.
$mail->Port = 465;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
$mail->SMTPAuth = true;
$mail->Username = 'notix';
// The SMTP password is a Notix API key.
$mail->Password = getenv('NOTIX_API_KEY');
$mail->setFrom('receipts@acme.com');
$mail->addAddress('customer@example.com');
$mail->Subject = 'Your receipt for order 4471';
$mail->isHTML(true);
$mail->Body = '<p>Thanks for your order.</p>';
$mail->AltBody = 'Thanks for your order.';
$mail->send();
A send that works once is easy. A send that survives a retry, a bad address and a network blip needs an idempotency key, a look at the exception it throws, and a webhook.
Pass the key as the second argument to send, create or batch; the SDK sends it as the Idempotency-Key header. The same key with the same body returns the original result instead of sending a second message; the same key with a different body throws NotUniqueException. Use the id of the business event: the order, the reset, the invoice.
Every API failure is { error: { code, message } } on the wire and a typed exception in PHP: BadRequestException for an unverified from domain or a malformed address, ForbiddenException when a sending-access key reaches an endpoint it cannot use, RateLimitedException when the per-second limit or the plan’s send limit is reached, with getRetryAfter() telling you how long to wait.
The API answers as soon as the message is queued. Delivery, bounces, complaints, opens and clicks arrive as email.* events on a webhook you register in the dashboard. Each request is signed with HMAC-SHA256 and carries X-Notix-Signature and X-Notix-Timestamp; Notix\Webhooks::constructEvent verifies both, within 300 seconds of the timestamp, before it decodes the body.
NOTIX_API_KEY for sending, NOTIX_WEBHOOK_SECRET for verifying. Give the app that only sends a sending-access key, and keep a full-access key for the jobs that manage domains and contacts.
<?php
require __DIR__ . '/vendor/autoload.php';
use Notix\Notix;
use Notix\Exception\NotixException;
$notix = new Notix(getenv('NOTIX_API_KEY'));
try {
// The second argument is the idempotency key: one key per business event,
// so a retry can never send twice.
$email = $notix->emails->send([
'from' => 'receipts@acme.com',
'to' => 'customer@example.com',
'subject' => 'Your receipt for order 4471',
'html' => '<p>Thanks for your order.</p>',
'text' => 'Thanks for your order.',
], 'order-4471-receipt');
} catch (NotixException $error) {
// getErrorCode(): BAD_REQUEST, FORBIDDEN, RATE_LIMITED, NOT_UNIQUE, ...
throw new RuntimeException($error->getErrorCode() . ': ' . $error->getMessage(), 0, $error);
}
// Delivery status arrives on your webhook as email.delivered, email.bounced, ...
echo 'queued ' . $email['emailId'] . PHP_EOL;
<?php
require __DIR__ . '/vendor/autoload.php';
use Notix\Exception\SignatureVerificationException;
use Notix\Webhooks;
$webhooks = new Webhooks(getenv('NOTIX_WEBHOOK_SECRET'));
try {
// The raw body and the request headers; the verifier checks
// X-Notix-Signature and X-Notix-Timestamp before decoding.
$event = $webhooks->constructEvent(file_get_contents('php://input'), getallheaders());
} catch (SignatureVerificationException $error) {
http_response_code(400);
exit('Invalid signature');
}
switch ($event['type']) {
case 'email.delivered':
case 'email.bounced':
case 'email.complained':
// $event['data']['id'] is the emailId you were given at send time.
break;
}
http_response_code(200);
echo 'ok';
That is the whole production surface for a single send. Batch sends of up to 100 messages, scheduling and cancelling are on the same emails resource; the production guide for transactional email walks through the same patterns in depth.
If your app already sends through PHPMailer, Symfony Mailer or a framework’s mail driver, the relay is the shortest path: change the host and credentials and nothing else. You give up the idempotency key and the batch endpoint, which only the API exposes, but every message still lands in the same log, honours the same suppression list and fires the same webhooks. The PHPMailer step above is the complete configuration; the same four values work in any SMTP client. Sending one-time codes? The OTP use case uses the verification API rather than a template of your own, and the deliverability check can run on any message before it goes out.
One JSON API for transactional and marketing email: send, batch, schedule, webhooks, typed SDKs.
GuidesThe SMTP transport in config/mail.php for Mailables, or the PHP SDK where you need idempotency keys and ids back.
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.
IntegrationsRelay credentials in WP Mail SMTP or any SMTP plugin, so every WordPress email goes out tracked and suppressed.
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, run the script. The free plan does not ask for a card.