← Back to blog
·8 min read·
Vanessa LozzardoVanessa Lozzardo

How to send email with PHP using an API or SMTP

Send email from PHP apps using the Sendkit SDK. Covers setup, HTML email, attachments, error handling, and SMTP.

phpemail-apitransactional-emailtutorial
How to send email with PHP using an API or SMTP

PHP's mail() function has been around forever, and it barely works. It depends on the server's local sendmail config, gives you zero feedback on delivery, and silently fails in ways that are almost impossible to debug. Every PHP developer has stared at mail() returning true while the email goes absolutely nowhere.

An email API is a better approach. You get structured responses, proper error codes, and delivery tracking without configuring a mail transfer agent on your server. This guide walks through sending email from PHP using the Sendkit email API, covering plain text, HTML, attachments, error handling, and SMTP as a fallback.

Installing the Sendkit PHP SDK

Pull in the package with Composer:

composer require sendkit/sendkit-php

Get an API key from your Sendkit dashboard. Keys starting with sk_live_ send real email. Keys starting with sk_test_ run the same validation but skip actual delivery, which is useful for automated tests and local development.

use Sendkit\Sendkit;

$client = Sendkit::client('sk_live_your_api_key');

Don't hardcode the key. Pull it from an environment variable or your framework's config:

use Sendkit\Sendkit;

$client = Sendkit::client(getenv('SENDKIT_API_KEY'));

Send a plain text email

The simplest case:

$response = $client->emails()->send([
    'from' => '[email protected]',
    'to' => '[email protected]',
    'subject' => 'Your order has shipped',
    'text' => 'Order #4821 shipped via FedEx. Tracking number: 7489201384.',
]);

echo $response['id']; // msg_8f2a...

The from address needs to be on a domain you've verified in Sendkit. If you haven't done that yet, the DMARC, DKIM, and SPF setup guide covers the DNS records you need.

Send HTML email

Most transactional emails need formatting. Always include a text version alongside html so clients that can't render HTML still show something readable.

function sendWelcomeEmail(string $email, string $name): string
{
    global $client;

    $response = $client->emails()->send([
        'from' => '[email protected]',
        'to' => $email,
        'subject' => "Welcome, {$name}",
        'text' => "Hey {$name}, thanks for signing up. Your account is ready.",
        'html' => <<<HTML
        <div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
            <h2>Welcome aboard, {$name}</h2>
            <p>Your account is set up and ready to go.</p>
            <p>A few things to try first:</p>
            <ul>
                <li>Complete your profile</li>
                <li>Connect your first integration</li>
                <li>Invite your team</li>
            </ul>
            <a href="https://app.yoursite.com/getting-started"
               style="background: #0066ff; color: white; padding: 12px 24px;
                      text-decoration: none; border-radius: 4px;
                      display: inline-block; margin-top: 16px;">
                Get started
            </a>
        </div>
        HTML,
    ]);

    return $response['id'];
}

Use inline CSS. Email clients strip <style> blocks and ignore external stylesheets, so inline styles are the only reliable approach across Gmail, Outlook, Apple Mail, and everything else.

Code on a monitor

Send email with attachments

Pass an attachments array where each item has a filename, content (base64-encoded), and content_type:

function sendInvoice(string $recipient, string $invoiceNumber, string $pdfPath): string
{
    global $client;

    $pdfContent = base64_encode(file_get_contents($pdfPath));

    $response = $client->emails()->send([
        'from' => '[email protected]',
        'to' => $recipient,
        'subject' => "Invoice #{$invoiceNumber}",
        'text' => "Invoice #{$invoiceNumber} is attached.",
        'html' => "<p>Invoice #{$invoiceNumber} is attached.</p>",
        'attachments' => [
            [
                'filename' => "invoice-{$invoiceNumber}.pdf",
                'content' => $pdfContent,
                'content_type' => 'application/pdf',
            ],
        ],
    ]);

    return $response['id'];
}

Keep total attachment size under 10MB. For larger files, upload them somewhere and include a download link. Big attachments slow delivery and trigger spam filters more often than you'd expect.

Send to multiple recipients

You can send to several addresses in one call:

$response = $client->emails()->send([
    'from' => '[email protected]',
    'to' => ['[email protected]', '[email protected]'],
    'subject' => '[ALERT] Database connection pool exhausted',
    'text' => 'Connection pool hit 100% at 14:32 UTC. Auto-scaling triggered.',
]);

For batch sends where each recipient gets different content, loop and send individually. Or use Sendkit's template system, which handles personalization server-side. The API docs cover template syntax.

Before sending to any list of addresses, validate them first. Sending to dead mailboxes and typos like gmial.com wrecks your sender reputation, and that damage affects every email you send afterward. Sendkit has built-in email validation that catches these problems. There's a separate guide on validating email addresses before sending if you want the details.

Error handling

The happy path is easy. The 2am page when your password reset flow silently breaks is not. Handle errors properly from the start.

use Sendkit\Sendkit;
use Sendkit\Exceptions\SendkitException;

$client = Sendkit::client(getenv('SENDKIT_API_KEY'));

function sendEmailSafely(array $params): array
{
    global $client;

    try {
        $response = $client->emails()->send($params);
        return ['success' => true, 'message_id' => $response['id']];

    } catch (SendkitException $e) {
        $status = $e->status;

        if ($status === 422) {
            // Bad input: invalid address, missing field
            return ['success' => false, 'error' => $e->getMessage(), 'retryable' => false];
        }

        if ($status === 429) {
            // Rate limited
            return ['success' => false, 'error' => 'rate_limited', 'retryable' => true, 'retry_after' => 60];
        }

        if ($status >= 500) {
            // Something broke on Sendkit's side
            return ['success' => false, 'error' => 'server_error', 'retryable' => true];
        }

        return ['success' => false, 'error' => $e->getMessage(), 'retryable' => false];

    } catch (\Exception $e) {
        // Network timeout, DNS failure, etc.
        return ['success' => false, 'error' => 'network_error', 'retryable' => true];
    }
}

For production, add retry logic with exponential backoff:

function sendWithRetry(array $params, int $maxRetries = 3): array
{
    for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
        $result = sendEmailSafely($params);

        if ($result['success'] || !($result['retryable'] ?? false)) {
            return $result;
        }

        $delay = $result['retry_after'] ?? min(pow(2, $attempt), 30);
        sleep($delay);
    }

    return ['success' => false, 'error' => 'max_retries_exceeded', 'retryable' => false];
}

Three retries handles most transient failures. If things are still broken after that, push the email into a queue (Redis, a database job table, whatever your app already uses) instead of blocking the request.

Code editor with PHP

Validate an email address before sending

If you accept email addresses from users (signup forms, contact forms, checkout), validate them before sending. A quick API call saves you from bouncing off dead mailboxes and trashing your sender reputation.

$result = $client->validateEmail('[email protected]');

if ($result['should_block']) {
    echo "Blocked: " . $result['block_reason'];
} else if ($result['is_valid']) {
    echo "Address looks good";
}

// The evaluations object has the details:
// $result['evaluations']['has_valid_syntax']
// $result['evaluations']['has_valid_dns']
// $result['evaluations']['mailbox_exists']
// $result['evaluations']['is_disposable']
// $result['evaluations']['is_role_address']
// $result['evaluations']['is_random_input']

This catches typos, disposable email services, and addresses where the mailbox doesn't exist. Worth running on every user-submitted address. More on this in the email validation guide.

Using SMTP instead of the API

If your codebase already uses PHPMailer or a framework's mail system, you don't need to rip it out. Sendkit provides an SMTP relay you can point at:

use PHPMailer\PHPMailer\PHPMailer;

$mail = new PHPMailer(true);

$mail->isSMTP();
$mail->Host = 'smtp.sendkit.com';
$mail->SMTPAuth = true;
$mail->Username = 'sendkit';
$mail->Password = 'sk_live_your_api_key';
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;

$mail->setFrom('[email protected]');
$mail->addAddress('[email protected]');
$mail->Subject = 'Password reset';
$mail->Body = '<p>Click <a href="https://app.yoursite.com/reset?token=abc123">here</a> to reset your password.</p>';
$mail->AltBody = 'Reset your password: https://app.yoursite.com/reset?token=abc123';

$mail->send();

Your API key is your SMTP password. No separate credentials.

The API is still the better choice for new code. It's faster (no SMTP handshake), returns a message ID immediately, and gives you structured error responses instead of SMTP reply codes you have to parse. But SMTP works well when you're plugging into an existing setup or a framework that expects it.

Sending raw MIME messages

If you're building email from raw MIME (maybe you're migrating from another provider and already have MIME construction code), Sendkit accepts that too:

$client->emails()->sendMime(
    envelopeFrom: '[email protected]',
    envelopeTo: '[email protected]',
    rawMessage: $mimeString,
);

This is a niche use case, but it's there when you need it. Most apps should stick with the structured send() method.

After the email leaves your server

Sending the email is the easy part. Getting it to the inbox is where the real work is.

First, set up domain authentication. DKIM, SPF, and DMARC are table stakes. Without them, Gmail and Outlook will spam-folder your messages or reject them outright. The DMARC/DKIM/SPF setup guide walks through the DNS records.

Second, watch your bounces. Repeatedly sending to addresses that don't exist will tank your sender reputation. Once that's damaged, it affects every email you send, not just the bad ones. Sendkit suppresses bounced addresses automatically, but keep an eye on bounce rates in your own system too.

Third, validate addresses before sending. If you accept user-submitted email addresses, a validation call catches typos and disposable addresses that will never engage.

The Sendkit docs cover webhooks, templates, and batch sending if you need to go deeper.

Wrapping up

Install sendkit/sendkit-php, call $client->emails()->send(), handle errors properly, and you have working email in your PHP app. The code itself is straightforward.

The ongoing work is deliverability. Authenticate your domain, don't ignore bounces, and validate addresses before sending. The sending code is a few lines. Keeping a good sender reputation is the actual job.

Share this article