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.
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, what DKIM does and what DMARC is.
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
function sendEmail(string $to, string $subject, string $text, ?string $html = null, ?string $idempotencyKey = null): array
{
$payload = [
'from' => 'Your App <[email protected]>',
'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
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 = '[email protected]';
$mail->Password = getenv('SMTP_PASSWORD');
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
$mail->Timeout = 15;
$mail->setFrom('[email protected]', 'Your App');
$mail->addAddress('[email protected]');
$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 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.
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:
- Log the full response from the API, or the
ErrorInfofrom PHPMailer. - Check the spam folder of the receiving address.
- Send the same message to a different provider, such as a Gmail address and an Outlook address.
- Open the received message's headers and look at the SPF, DKIM and DMARC results.
- Try a temporary inbox 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 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. The API guide 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.
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.