# How to send email from PHP with cURL or PHPMailer

> Send email from PHP on your own domain using an HTTP API with cURL or SMTP with PHPMailer, with timeouts, error handling and the mistakes to avoid.

Published 2026-10-23. Source: https://mailions.com/blog/send-email-php

PHP's built-in `mail()` function is the reason so many websites have email trouble. It hands the message to the local server with no authentication, no DKIM signature and no way to tell you what went wrong. The fix is to stop using it. Here are two better routes, with code you can paste in.

## Why `mail()` lets you down

Calling `mail()` returns `true` as soon as the web server accepts the message. That tells you nothing about delivery. Receiving providers see an unsigned message from a shared hosting IP, and many send it to spam. Some hosts disable the function entirely.

Real mail needs a sender that authenticates, a domain with SPF, DKIM and DMARC published, and a status you can read. Start with the domain: [what an SPF record is](/blog/what-is-spf-record), [what DKIM does](/blog/what-is-dkim) and [what DMARC is](/blog/what-is-dmarc).

## Option 1: the HTTP API with cURL

PHP's cURL extension is on nearly every host. You send one JSON `POST` with a Bearer token.

```php
<?php
function sendEmail(string $to, string $subject, string $text, ?string $html = null, ?string $idempotencyKey = null): array
{
    $payload = [
        'from' => 'Your App <noreply@yourdomain.com>',
        'to' => $to,
        'subject' => $subject,
        'text' => $text,
    ];
    if ($html !== null) {
        $payload['html'] = $html;
    }

    $headers = [
        'Authorization: Bearer ' . getenv('MAIL_API_KEY'),
        'Content-Type: application/json',
    ];
    if ($idempotencyKey !== null) {
        $headers[] = 'Idempotency-Key: ' . $idempotencyKey;
    }

    $ch = curl_init(getenv('MAIL_API_URL') . '/api/v1/emails');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_POSTFIELDS => json_encode($payload),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 15,
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = curl_error($ch);
    curl_close($ch);

    if ($body === false) {
        throw new RuntimeException('Mail request failed: ' . $error);
    }
    $data = json_decode($body, true) ?: [];
    if ($status >= 400) {
        $code = $data['error']['code'] ?? $status;
        $message = $data['error']['message'] ?? 'send failed';
        throw new RuntimeException("$code: $message");
    }
    return $data;
}
```

Three habits worth keeping. The timeout stops a slow network from freezing the page. The key sits in an environment variable and never in source control. And the `from` address must be on the domain the API key belongs to.

A `202` with `status: queued` means the mail server was briefly unavailable and the message is stored for sending later, for up to 24 hours. Don't resend it. If you retry after a timeout, pass the same idempotency key (for example `welcome/` plus the user ID) and no second copy goes out.

## Option 2: SMTP with PHPMailer

Install PHPMailer with Composer: `composer require phpmailer/phpmailer`. Then:

```php
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;

$mail = new PHPMailer(true);
try {
    $mail->isSMTP();
    $mail->Host = 'mail.example.com';
    $mail->SMTPAuth = true;
    $mail->Username = 'noreply@yourdomain.com';
    $mail->Password = getenv('SMTP_PASSWORD');
    $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
    $mail->Port = 587;
    $mail->Timeout = 15;

    $mail->setFrom('noreply@yourdomain.com', 'Your App');
    $mail->addAddress('ada@example.com');
    $mail->Subject = 'Welcome aboard';
    $mail->Body = 'Thanks for signing up.';
    $mail->send();
} catch (Exception $e) {
    error_log('Mail failed: ' . $mail->ErrorInfo);
}
```

Use the host name your provider gives you instead of `mail.example.com`. Port 587 with STARTTLS is the usual setup, and [SMTP ports explained](/blog/smtp-ports-explained) shows why. For an HTML message add `$mail->isHTML(true);` and set `AltBody` to a plain text version.

## Which should you use?

Choose **cURL and the API** when your host blocks SMTP ports, when you want a status back, or when you want idempotency keys. Choose **PHPMailer** when a framework or CMS already expects SMTP settings, or when you want one code path that works against any mail server.

Laravel and WordPress both use SMTP under the hood. For WordPress, read [WordPress not sending email](/blog/wordpress-not-sending-email).

## Plain text and HTML together

Send both versions of the body where you can. The HTML part gives you formatting, and the plain text part is what spam filters, smartwatches and people with images switched off will read. In the API, pass `text` and `html` in the same request. In PHPMailer, set `Body` to the HTML, `AltBody` to the text and call `isHTML(true)`. Keep the two saying the same thing. A text part that reads "view this email in your browser" while the HTML has the real content looks like a template, and filters treat it that way.

## Sending to more than one person

For a receipt, the customer is the only `to` address, and your own bookkeeping address can go in `bcc`. The API accepts up to 50 recipients across `to`, `cc` and `bcc`, and every distinct address counts against your monthly allowance. Avoid putting a whole customer list in `to` or `cc`: everyone can see everyone else's address, which is a privacy problem as well as a deliverability one. Send one message per person.

## Debugging a message that didn't arrive

Work down this list before you change any code:

1. Log the full response from the API, or the `ErrorInfo` from PHPMailer.
2. Check the spam folder of the receiving address.
3. Send the same message to a different provider, such as a Gmail address and an Outlook address.
4. Open the received message's headers and look at the SPF, DKIM and DMARC results.
5. Try a [temporary inbox](/temp) to prove the sending side works at all.

If step 4 shows a failure, the code isn't the problem. DNS is. [What is DMARC](/blog/what-is-dmarc) explains how the three results fit together.

## Common mistakes

- **Putting credentials in the repository.** Use environment variables and rotate anything that leaks.
- **Sending from the visitor's address.** For contact forms, send from your own address and put theirs in `reply_to`.
- **Ignoring the return value.** Check every response, and log the error message.
- **Sending inside the page request.** For anything non-urgent, queue it and send from a worker.

## Next steps

For a Node.js version of the same request, see [sending email from Node.js](/blog/send-email-nodejs). The [API guide](/help/sending-api) has full PHP examples including a verification-code email. With Mailions the sender can be any address on a domain you have verified, no mailbox needed. To get a trial for your domain, [contact us](/contact).

## Frequently asked questions

### Why is PHP mail() not working?

`mail()` hands messages to the server's local mail system, which is often not configured, blocked by the host, or sending without SPF and DKIM for your domain. Messages then vanish or land in spam. Use authenticated SMTP or an email API instead.

### How do I send email with PHPMailer?

Install it with Composer, create a PHPMailer object, enable SMTP, set the host, port 587, STARTTLS and your login, then set the sender, recipient, subject and body and call `send()`. Read the exception message when it fails.

### Can I send HTML email from PHP?

Yes. With PHPMailer, call `isHTML(true)` for the HTML body and also set a plain-text `AltBody`. With an HTTP API, send both `html` and `text` fields. A text version helps accessibility and deliverability.
