Debugging WordPress SMTP OAuth setups with Postman

(Postman here is the API platform at postman.com, not Post SMTP, the WordPress mailer plugin formerly called Postman.)

Why validate the OAuth registration in Postman before the plugin

A mailer plugin’s connection screen is a poor debugging surface. WP Mail SMTP, FluentSMTP, and Post SMTP all reduce a dozen different OAuth-side failures to the same “Authentication failed” banner or Connection Test error. The OAuth token endpoint is not so reticent: it returns precise error strings, in the exact wording the reader can search for.

Getting the token in Postman first localises the failure. If Postman receives a token but the plugin still fails, the problem is scope, transport, or the plugin’s own OAuth handling. If Postman itself fails, the problem is the OAuth registration (redirect URI, client secret, consent, tenant policy) and the mailer plugin will fail the same way for the same reason. One question at a time.

The two paths this piece covers are the two OAuth flows WordPress mailer plugins actually use: Microsoft Entra ID (for Microsoft 365 SMTP or Graph submission) and Google Cloud OAuth (for Workspace Gmail submission). Both walkthroughs assume Postman is open on the Authorization tab of a request, with the matching admin-console page (Entra app registration or Google Cloud OAuth client) beside it.

What Postman validates, and what it doesn’t

Postman speaks HTTP. It obtains an access token from the OAuth token endpoint, attaches the token to an HTTP request, and reports what the API returns. That is the full extent of the validation.

Postman does not exercise SMTP submission. If the mailer plugin uses SMTP AUTH with XOAUTH2 (unusual in 2026, but real for the residual M365 SMTP AUTH path and a handful of Workspace setups), the token Postman obtains still needs to survive the SMTP handshake. The tool for that test is swaks with the --auth-plain-xoauth2 construction, not Postman. Postman confirms the token is issued with the correct claims; the SMTP handshake is a separate confirmation.

Postman also does not exercise the Microsoft Graph or Gmail API submission itself. It exercises an adjacent endpoint that fires the same authorisation path, which is enough to confirm the token has the right permissions. The distinction matters when reading the errors: Postman’s 200 on GET /v1.0/me proves the token, not the plugin.

The callback URL setup that trips both flows

Postman uses a fixed public callback by default: https://oauth.pstmn.io/v1/callback. That URL must be registered as a redirect URI on the app registration or OAuth client before the browser round-trip will complete.

Forgetting this step produces AADSTS50011: The redirect URI specified in the request does not match the redirect URIs configured for the application from Microsoft, or Error 400: redirect_uri_mismatch from Google. Both are Postman-specific in the sense that they name a URL the operator never sees inside the mailer plugin’s own configuration; the plugin has its own redirect URI, registered separately.

Register https://oauth.pstmn.io/v1/callback on the app registration alongside the plugin’s own redirect URI. On a single-tenant Azure app or a single-project Google Cloud OAuth client the two coexist without side effects, and most operators leave both registered permanently for the next debug cycle.

For tenants whose security policy locks redirect URIs to corporate domains, toggle “Authorize using browser” in Postman’s OAuth 2.0 settings. Postman opens the system browser, receives the redirect via the Postman desktop agent, and shows the callback URL in the Callback URL field once the toggle is on. On macOS and Linux the desktop agent must be installed and running; the web app cannot receive a redirect from an external browser.

Validating a Microsoft 365 OAuth registration in Postman

The M365 mailer plugins all submit through Microsoft Graph, but the scope set each requests differs. FluentSMTP’s Outlook mailer requests user.read mail.readwrite mail.send mail.send.shared offline_access (verified in the plugin’s Outlook API.php); it asks for Mail.ReadWrite alongside Mail.Send. WP Mail SMTP Pro’s Outlook mailer requests Mail.ReadWrite for a related reason: the plugin’s docs say the broader scope lifts the 3MB Mail.Send attachment cap. Post SMTP ships an Office 365 mailer, though its modern Graph scope set is not readily confirmable from its public source. In practice, granting Mail.Send, Mail.ReadWrite, and offline_access delegated permissions on the app registration satisfies all three named plugins.

Prerequisites: an Entra ID app registration with the Postman callback registered as a redirect URI, the app’s Application (client) ID and a client secret value (not the Secret ID) copied out at creation time, and admin consent granted on the scopes above.

In Postman, open a request that targets https://graph.microsoft.com/v1.0/me, switch to the Authorization tab, and pick OAuth 2.0 as the type. Fill in:

  • Token Name: any label, for example m365-mailsend-token
  • Grant Type: Authorization Code (With PKCE)
  • Callback URL: https://oauth.pstmn.io/v1/callback, with “Authorize using browser” unchecked
  • Auth URL: https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize, replacing {tenant} with the tenant ID (the plugin uses the same value in its OAuth flow)
  • Access Token URL: https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
  • Client ID: the Application (client) ID
  • Client Secret: the client secret value
  • Code Challenge Method: SHA-256
  • Code Verifier: leave blank; Postman generates one
  • Scope: Mail.Send Mail.ReadWrite offline_access matches what FluentSMTP and WP Mail SMTP Pro’s Outlook mailer request. For a Post SMTP or custom integration that only calls sendMail, narrow to Mail.Send offline_access. offline_access is what makes Microsoft return a refresh token; without it the token expires in an hour and the plugin has no way to renew.
  • State: leave blank; Postman validates the round trip
  • Client Authentication: Send as Basic Auth header (the default for Entra ID)

Click “Get New Access Token”. The browser opens, Microsoft authenticates the user and prompts for consent (or skips consent if admin consent has been granted tenant-wide), and Postman receives the access token and refresh token. A 200 from https://graph.microsoft.com/v1.0/me confirms the registration works end to end.

Transcribe the same client ID and client secret value into the mailer plugin’s connection screen. The plugin will run its own browser round-trip and obtain its own refresh token; the token Postman received is not what the plugin uses. What Postman proved is that the round-trip works.

The rarer path: SMTP AUTH with OAuth

If the site is on the M365 SMTP AUTH path and using a mailer that speaks XOAUTH2 over SMTP submission (unusual, but real for tenants that cannot use Graph), the scope is different: https://outlook.office.com/SMTP.Send offline_access. A Graph-scoped token is rejected by smtp.office365.com with 535 5.7.3 Authentication unsuccessful. The fix is to obtain a separate token with SMTP.Send scope, or to switch the integration to Graph’s sendMail endpoint.

The rarer path: application permissions

Where the app registration uses application-only Graph permissions (WP Mail SMTP Pro’s setup docs cover this for organisations that prefer app-only over delegated), swap the Postman grant type to Client Credentials, drop the auth URL and code-challenge fields, and set the scope to https://graph.microsoft.com/.default (application permissions use .default, not individual scope names). The token this returns carries application permissions only; GET /v1.0/me returns 403 Forbidden because there is no user context. Test against https://graph.microsoft.com/v1.0/users/{id} where {id} is the mailbox the WordPress site sends as.

Validating a Google Workspace OAuth registration in Postman

The Workspace mailer plugins all authenticate against the Gmail API rather than SMTP AUTH. WP Mail SMTP, FluentSMTP, and Post SMTP each call POST https://gmail.googleapis.com/gmail/v1/users/me/messages/send under the hood. They request different Gmail scopes:

  • WP Mail SMTP requests https://mail.google.com/ (its Gmail Auth.php calls setScopes([Gmail::MAIL_GOOGLE_COM]))
  • FluentSMTP requests https://www.googleapis.com/auth/gmail.compose
  • Post SMTP requests https://www.googleapis.com/auth/gmail.send

All three tokens send mail; the difference is what the plugin’s Connection Test can read back. Match Postman’s scope to the plugin being validated, otherwise the tokeninfo scope check below is meaningless.

Prerequisites: a Google Cloud project with the Gmail API enabled, an OAuth 2.0 Client ID (type: Web application) with the Postman callback registered under Authorized redirect URIs, the client ID and client secret copied out of the OAuth client detail page, and the OAuth consent screen configured with the same Gmail scope the target plugin requests. The Workspace SMTP guide walks through the Google Cloud side.

In Postman, target GET https://gmail.googleapis.com/gmail/v1/users/me/profile and configure OAuth 2.0:

  • Token Name: workspace-gmail-token
  • Grant Type: Authorization Code (With PKCE)
  • Callback URL: https://oauth.pstmn.io/v1/callback
  • Auth URL: https://accounts.google.com/o/oauth2/v2/auth
  • Access Token URL: https://oauth2.googleapis.com/token
  • Client ID: from the OAuth 2.0 Client detail page
  • Client Secret: from the same page
  • Code Challenge Method: SHA-256
  • Scope: the scope the target plugin requests, from the list above
  • Client Authentication: Send client credentials in body. Google’s token endpoint accepts either, and Google’s own web-server flow documentation uses the body form; matching what the docs show avoids the interop surprises that turn up when Postman signs an unexpected Basic header.

Below the field list Postman groups extra parameters under “Advanced Options”; expand the “Auth Request” sub-panel and add two Google-specific parameters as key-value pairs:

  • access_type = offline; required for Google to issue a refresh token
  • prompt = consent; forces the consent screen to render even on repeat authorisations, which is what makes Google issue a fresh refresh token. Google issues one only on first consent otherwise, so the second Postman run silently returns an access-only response and the operator misses that no refresh token arrived.

Click “Get New Access Token”. A browser window opens, Google authenticates the user and shows the consent screen, and Postman receives the token pair. A 200 from GET https://gmail.googleapis.com/gmail/v1/users/me/profile confirms the registration.

Transcribe the client ID and client secret into the mailer plugin’s Google connection screen. As with M365, the plugin obtains its own refresh token during its own browser round-trip; the Postman token is not shared.

The Testing-mode consent screen trap

Google’s OAuth consent screen has two publishing states. In Testing mode, Google issues refresh tokens that expire after seven days. A mailer plugin configured against a Testing-mode consent screen works for a week and then fails silently: the refresh call returns invalid_grant, the plugin catches the error, and the next email is discarded.

The Postman round-trip does not fail differently under Testing mode. The token endpoint returns a refresh token; nothing marks it as short-lived. The check is on the Google Cloud console side: OAuth consent screen → Publishing status = In production. Move the consent screen to production before pasting credentials into the mailer plugin, or accept that the plugin is on a seven-day timer.

Reading the token to prove the scope landed

The most common cause of “Postman worked, plugin still fails” is a scope mismatch that Postman does not surface. The token endpoint returns an access token; the endpoint the reader tested against accepted it; the mailer plugin will use a different endpoint that needs a different permission.

For Microsoft Entra tokens, the access token is a JWT (JSON Web Token) and can be inspected directly. Copy the access token value from Postman’s token dialog, paste into jwt.ms (Microsoft’s official verifier), and read two claims:

  • scp; the delegated scopes on the token, space-separated. For the FluentSMTP / WP Mail SMTP Pro Graph path this should include Mail.ReadWrite and Mail.Send. For the SMTP AUTH-with-OAuth path it should include SMTP.Send.
  • roles; the application permissions on the token (only present on Client Credentials tokens). For app-only Graph submission this should include Mail.Send.

If the expected claim is missing, the app registration did not grant it, the consent flow did not accept it, or the request asked for a different scope. Fix at the source, get a new token, decode again.

For Google tokens, the access token is opaque (Google does not issue JWTs for OAuth access tokens by default). Verify the scope by calling Google’s tokeninfo endpoint:

GET https://oauth2.googleapis.com/tokeninfo?access_token=<the access token from Postman>

The response includes a scope field. Confirm it contains the scope the target plugin will request: https://mail.google.com/ for WP Mail SMTP, https://www.googleapis.com/auth/gmail.compose for FluentSMTP, https://www.googleapis.com/auth/gmail.send for Post SMTP. A response showing only userinfo.email or a bare set of profile scopes indicates the consent screen was misconfigured and the Gmail scope was never granted.

Named failures, mapped to what the mailer plugin will show

The OAuth-side errors below turn up frequently while validating in Postman, and each maps to a distinct symptom in the mailer plugin. Reading them alongside the plugin’s failure narrows the fix.

OAuth-side error Plugin symptom Fix
AADSTS50011: redirect URI does not match Plugin browser round-trip never returns; connection screen hangs Add the redirect URI (plugin’s or Postman’s) to the Entra app registration
AADSTS65001: user or administrator has not consented Browser returns to WordPress with an error; Connection Test fails Grant admin consent in Entra → API permissions, or add prompt=consent under Advanced Options for a fresh Postman consent prompt
AADSTS70011: invalid scope Same “Authentication failed”; actual submission fails on the send call Match Postman’s scope to what the plugin asks for; don’t mix individual Graph scopes with .default in the same request
invalid_client “Authentication failed” on Save (or on Connection Test if the plugin defers the token call) Wrong secret value, expired secret (Azure caps secret lifetime at 24 months with no expiry-warning email), or Basic-vs-body auth mismatch. Flip Postman’s Client Authentication and retry
interaction_required / AADSTS50076 Browser round-trip completes but token call fails Conditional Access requires MFA or a managed device: tenant policy, not Postman. Fix with a CA exclusion for the WordPress mailbox or a service-principal configuration
invalid_grant on refresh (Google) Worked yesterday, fails today, no configuration changed Testing-mode consent screen (7-day expiry), user revoked at myaccount.google.com/permissions, six-month idle window, or consent scopes were edited (invalidates prior refresh tokens). Regenerate with prompt=consent; publish the consent screen if Testing mode is the cause
5.7.14 from smtp.gmail.com Appears verbatim in the plugin’s send log (SMTP AUTH XOAUTH2 paths only) XOAUTH2 token missing the https://mail.google.com/ scope. Postman doesn’t reproduce this; the tokeninfo scope check above catches the underlying cause
Token works in Postman, plugin still fails on send Silent send failure, or the same “Authentication failed” Scope mismatch or transport mismatch. Decode the returned token (jwt.ms or tokeninfo), confirm the claim matches the plugin’s request, and match Postman’s scope to the plugin (validating with Mail.Send while the plugin requests Mail.ReadWrite returns a token that looks fine and fails in the plugin)

One tip worth keeping

Store the client secret in a Postman environment variable, not in the OAuth configuration inline. Reference it as {{m365_client_secret}} (or the Google equivalent) in the Client Secret field. Otherwise every collection export ships the secret in plaintext inside the JSON, and the export travels with the collection to anyone the operator shares it with. Environment values do not travel with the collection unless the environment is exported alongside; keeping the secret there is the difference between “the debug collection is portable” and “the debug collection leaks Azure credentials”.

Debugging WordPress SMTP OAuth setups with Postman details
Short Definition
The Postman API platform used as a diagnostic tool for the OAuth registration behind a WordPress SMTP mailer plugin. Obtain the token the plugin will consume, verify its scope claim, and localise a plugin-side authentication failure to the registration, the scope, or the transport.