To send WordPress email through SendGrid, hook into phpmailer_init and point the underlying PHPMailer instance at SendGrid's SMTP relay. WordPress hands you the live PHPMailer object by reference, so you reconfigure it in place: turn on SMTP, set the host, the port, and authentication, then let wp_mail() carry on as normal. Drop this into a must-use plugin:
add_action( 'phpmailer_init', 'te_configure_sendgrid' );
function te_configure_sendgrid( $phpmailer ) {
$phpmailer->isSMTP();
$phpmailer->Host = 'smtp.sendgrid.net';
$phpmailer->Port = 587;
$phpmailer->SMTPAuth = true;
$phpmailer->SMTPSecure = 'tls';
$phpmailer->Username = 'apikey'; // the literal string, not your key
$phpmailer->Password = defined( 'TE_SENDGRID_API_KEY' ) ? TE_SENDGRID_API_KEY : '';
// From must be an address you have verified in SendGrid.
$phpmailer->setFrom( 'no-reply@example.com', 'Example Site' );
}The one detail people fumble is the username. SendGrid's SMTP relay uses the literal string apikey as the username for every account; your actual key goes in the password field. It is not your SendGrid login email, and it is not the API key in both fields. Username is always the word apikey.
That is the whole switch. From this point every wp_mail() call, the password reset, the new-comment notification, a WooCommerce order email, a contact-form submission, leaves through SendGrid's authenticated relay instead of your host's local mailer.
Why not just use PHP mail()?
By default wp_mail() falls back to PHP's mail() function, which shells out to the local mail transfer agent (usually sendmail or postfix) on the web server. It works in the sense that the function returns true and a message leaves the box. Whether it reaches anyone's inbox is a different question, and the answer on shared and cloud hosting is usually no.
The problems are all about authentication and reputation:
- No SPF or DKIM alignment. A message sent through your web host's local MTA is sent from an IP and a domain that almost never line up with the
From:header. Receiving servers check SPF (is this IP allowed to send for this domain?) and DKIM (is this message cryptographically signed by the domain?). Localmail()typically passes neither, so the message looks forged. - Shared-IP reputation you do not control. On shared hosting your mail leaves from an IP pool used by hundreds of other sites, some of them spammers. You inherit their reputation. One bad neighbor and your password-reset emails are in the spam folder or silently dropped.
- No bounce, complaint, or open visibility. PHP
mail()returns success the instant it hands off to the local MTA. You have no idea whether the message was delivered, bounced, or marked as spam. Debugging "the customer says they never got the receipt" is guesswork. - Hosts increasingly block port 25 outright. Plenty of cloud providers (and a growing number of shared hosts) firewall outbound port 25 to fight spam, so
mail()does not even leave the building.
A reputable email provider fixes all of this. SendGrid sends from warmed, monitored IPs, signs every message with DKIM for your domain, aligns SPF, and gives you delivery, bounce, and spam-complaint data. The article uses SendGrid, but the same reasoning (and almost the same snippet) applies to any provider with an SMTP relay: Amazon SES, Mailgun, Postmark, Brevo. The point is that transactional mail should leave through an authenticated, accountable relay, not the web server's local sendmail.
Storing the API key (wp-config.php first, .env as the alternate)
The snippet above reads the key from a TE_SENDGRID_API_KEY constant rather than pasting the literal key into the plugin. Never hardcode a credential into a PHP file that lives in the webroot and ships to version control. Define it in wp-config.php instead, above the /* That's all, stop editing! */ line:
// wp-config.php
define( 'TE_SENDGRID_API_KEY', 'SG.your-real-key-goes-here' );wp-config.php is the right home for this on a standard WordPress install. It sits outside the document root on a well-configured host, it is already where every other secret lives (database password, auth salts), and it is loaded before any plugin runs, so the constant is defined by the time phpmailer_init fires. The plugin's defined() check means a missing constant degrades gracefully (empty password, auth fails loudly in the SendGrid logs) instead of throwing a fatal error.
If you prefer to keep secrets out of wp-config.php entirely, the alternate setup is a .env file loaded at the top of wp-config.php. That keeps the key in a single gitignored file shared across environments, which is tidier for a team or a Composer-managed site. I walk through the loader and the getenv() wiring in keeping WordPress secrets in a .env file. Either way the plugin code does not change: it reads TE_SENDGRID_API_KEY, and how that constant gets defined is a setup choice.
Whichever you pick, scope the key in SendGrid to Mail Send only. There is no reason a WordPress site needs a full-access key, and a Mail-Send-only key that leaks cannot be used to read your contacts or rewrite your sender authentication.
Sender authentication is the part that actually decides inbox vs. spam
Switching to SendGrid's relay is necessary but not sufficient. If you send from a From: address on a domain you have not authenticated in SendGrid, the mail still leans on SendGrid's shared sending domain, and deliverability is mediocre. The step that moves mail into the inbox is domain authentication: you tell SendGrid which domain you send from, and it gives you DNS records to publish.
In the SendGrid dashboard under Sender Authentication, authenticating a domain produces a set of CNAME records (SendGrid's modern setup uses CNAMEs that point at SendGrid-hosted SPF and DKIM data). You add them to your domain's DNS, SendGrid verifies them, and from then on:
- DKIM signs every outgoing message with a key published in your DNS, so receivers can verify the message genuinely came from your domain and was not altered.
- SPF authorizes SendGrid's servers to send on behalf of your domain.
- The
From:address you use in the snippet (no-reply@example.com) now aligns with the authenticated domain, which is what DMARC checks and what mailbox providers reward.
Until those records verify, expect inconsistent placement no matter how clean your relay setup is. The setFrom() address in the plugin must be on a domain you have authenticated; sending as no-reply@yourdomain.com while only gmail.com is verified defeats the entire exercise.
The Web API alternative (briefly)
SendGrid also offers a Web API v3, where instead of speaking SMTP you POST JSON to https://api.sendgrid.com/v3/mail/send with a Bearer token. The API is the better choice at high volume or when you want richer features: per-message tracking settings, scheduled sends, templates, and lower latency because there is no SMTP handshake per message.
For dropping into WordPress, though, SMTP is the simplest path. The phpmailer_init hook above needs no extra library, no Composer dependency, and no rewrite of how WordPress builds the message; it just redirects the transport. To use the Web API you would typically install SendGrid's PHP SDK (via Composer, which suits a Composer-autoloaded must-use plugin) and short-circuit wp_mail() with the pre_wp_mail filter, which is more moving parts than most sites need. Start with SMTP; reach for the API only when you have a concrete reason to.
Verify it actually sends
Configuration that returns true from wp_mail() is not proof anything arrived. Confirm the round trip:
- Send a real test. Trigger any WordPress email: request a password reset for a test account, or run a quick
wp_mail()from WP-CLI:
wp eval "var_dump( wp_mail( 'you@example.com', 'SendGrid test', 'Routed through SendGrid.' ) );"- Check it arrives, and check the headers. Open the message in your inbox and view the original/raw headers. You want to see
Received:lines throughsendgrid.net, andSPF=passplusDKIM=passfor your domain. Gmail's "Show original" reports both in plain language. - Confirm it in SendGrid Activity. The Activity Feed in the SendGrid dashboard logs every message with its status: processed, delivered, bounced, or dropped. If
wp_mail()returnedtruebut Activity shows nothing, the message never reached SendGrid (almost always a bad key or the username not set to the literalapikey). If Activity shows a bounce, the problem is downstream (recipient, content, or unverified domain), not your WordPress wiring.
If wp_mail() returns false, turn on PHPMailer's SMTP debug temporarily by setting $phpmailer->SMTPDebug = 2; inside the hook and check the server error log; an authentication failure there points straight at the key or username.
A complete must-use plugin
Putting it together as a single file at wp-content/mu-plugins/te-sendgrid-mailer.php. Must-use plugins load automatically and survive theme switches, so your mail routing does not vanish when someone changes themes:
<?php
/**
* Plugin Name: TE SendGrid Mailer
* Plugin URI: https://techearl.com/wordpress-send-email-sendgrid
* Description: Routes wp_mail() through SendGrid's SMTP relay for authenticated, deliverable transactional email.
* Version: 1.0.0
* Author: Ishan Karunaratne
* Author URI: https://techearl.com
* License: GPL-2.0-or-later
* Text Domain: te-sendgrid
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_action( 'phpmailer_init', 'te_configure_sendgrid' );
function te_configure_sendgrid( $phpmailer ) {
if ( ! defined( 'TE_SENDGRID_API_KEY' ) || '' === TE_SENDGRID_API_KEY ) {
return; // No key configured; leave the default mailer in place.
}
$phpmailer->isSMTP();
$phpmailer->Host = 'smtp.sendgrid.net';
$phpmailer->Port = 587;
$phpmailer->SMTPAuth = true;
$phpmailer->SMTPSecure = 'tls';
$phpmailer->Username = 'apikey';
$phpmailer->Password = TE_SENDGRID_API_KEY;
$phpmailer->setFrom( 'no-reply@example.com', 'Example Site' );
}Set TE_SENDGRID_API_KEY in wp-config.php, change the setFrom() address to a domain you have authenticated in SendGrid, and that is a complete, version-control-safe email setup for the whole site.
See also
- Keep WordPress Secrets in a .env File: the alternate setup for the SendGrid key, with the loader wiring and
getenv()pattern for keeping credentials out ofwp-config.php - A Composer-Autoloaded Must-Use Plugin: how to wire up Composer in an mu-plugin, which is the natural home if you move from the SMTP hook to SendGrid's Web API SDK
- Clean Up wp_head in WordPress: the same must-use-plugin pattern applied to trimming default head output, another one-file fix that survives theme switches
- How to Optimize WooCommerce: order and account emails are exactly the transactional mail that has to land, so reliable SendGrid delivery pairs with the store-performance work here
Sources
Authoritative references this article was fact-checked against.
- Integrating with the SMTP API: SendGrid (Twilio) documentationtwilio.com
- phpmailer_init hook: WordPress Developer Referencedeveloper.wordpress.org
- wp_mail(): WordPress Developer Referencedeveloper.wordpress.org





