Notix
Guides

Send email from PHP with one API call.

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.

What you need

Three things, none of them a card.

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.

A verified domain.

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.

PHP 8.1 or later.

With the curl and json extensions. The SDK is one Composer install:

terminal
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.

Steps

From an empty file to a queued message.

  1. Construct the client.

    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.

    notix.php
    <?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');
    
  2. Send one email.

    $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.

    send.php
    <?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;
    
  3. Keep the id, catch the exceptions.

    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().

    errors.php
    <?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());
    }
    
  4. Or send through PHPMailer.

    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.

    phpmailer.php
    <?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();
    
In production

The three lines that separate a demo from a system.

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.

Idempotency key.

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.

The exceptions.

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.

Delivery status by webhook.

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.

Environment and keys.

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.

send-with-idempotency.php
<?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;
webhook.php
<?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.

Using SMTP instead

When the mailer is already wired, keep it.

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.

FAQ

Questions, answered.

Do I need the SDK, or can I call the API from PHP directly?
Either. The SDK is a thin wrapper over the same JSON endpoints, so the cURL example on this page sends exactly what $notix->emails->send sends. Use the SDK when you want typed exceptions, the idempotency key argument and the webhook verifier; use cURL when you would rather not add a Composer dependency. Both need only the curl and json extensions.
Is this a PHPMailer alternative?
It works with PHPMailer rather than replacing it. Point PHPMailer at smtp.usenotix.dev with the username notix and an API key as the password, and everything PHPMailer already does keeps working. The relay turns the SMTP session into the same tracked, suppressed send the API makes. If you want idempotency keys, batch sends and ids back, the SDK and the API expose those directly.
Which PHP version does the SDK need?
PHP 8.1 or newer, with the curl and json extensions. It ships a Laravel service provider and facade that are discovered automatically, and it works the same way in any other PHP project through the plain Notix\Notix client shown on this page. The Laravel guide covers the framework-specific setup.
What happens when a send fails?
An API error throws a Notix\Exception\NotixException subclass named for the status: BadRequestException for a malformed address or unverified from domain, ForbiddenException for a key without the permission, RateLimitedException with getRetryAfter() for 429, NotUniqueException when an idempotency key is reused with a different body. A network failure throws ConnectionException instead, and is safe to retry with the same key.
Can I send more than one email per request?
Yes. $notix->emails->batch takes up to 100 messages in one request and returns the list of created email ids, and accepts an idempotency key as its second argument like send does. Scheduling, reading a message back and cancelling a scheduled send are on the same emails resource.

Send the first one now.

Verify a domain, copy an API key, run the script. The free plan does not ask for a card.