There is a specific, maddening version of this problem: Gmail sending works perfectly, and then roughly a week later it stops. You reconnect, it works again, and a week later it stops again.
The error, if your plugin surfaces it, looks like this:
{ "error": "invalid_grant", "error_description": "Token has been expired or revoked." }
Nothing is wrong with your site. The cause is a single setting in the Google Cloud console, and it is the default.
The seven-day cause
When you create OAuth credentials in Google Cloud, the consent screen has a publishing status, and new projects start in Testing.
Refresh tokens issued by an app in Testing status expire after seven days. That is the entire mechanism. Your site holds a refresh token so it can keep sending without you re-authorising; on day seven Google invalidates it, the next send fails with invalid_grant, and mail stops.
It is a poor failure mode, because a week is long enough that the connection feels established. People reconnect, watch it work, and conclude the problem was transient.
The fix
- Open the Google Cloud console and select your project.
- Go to APIs & Services → OAuth consent screen.
- If publishing status is Testing, click Publish app to move it to In production.
- Reconnect your site to get a fresh token. The old one is already dead.
If the app only ever accesses your own Google Workspace accounts and uses restricted scopes, you may see a verification warning. For an internal-use application sending your own mail this does not block the change, and an app registered as Internal within a Workspace organisation skips it entirely.
The other causes of invalid_grant
Publishing status is the common one. If yours is already In production, work through these.
The Google account password was changed
Changing the password on the Google account that authorised the connection invalidates all its OAuth tokens immediately. Mail stops the moment somebody rotates a password, which makes the timing look unrelated to email.
This is a strong argument for authorising with a dedicated sending account rather than a person’s own — a departing employee’s password reset should not stop your order confirmations.
Six months of inactivity
A refresh token unused for six months is revoked. This affects low-traffic sites and staging environments that send rarely.
Too many tokens
There is a limit of 50 refresh tokens per user per OAuth client. Passing it starts silently invalidating the oldest. Repeatedly disconnecting and reconnecting while troubleshooting can quietly cause this.
The site was cloned
Tokens are bound to the site that authorised them. A staging copy of production holds a token it cannot use. This is worth understanding as a safety feature rather than a bug — it is one reason a clone does not immediately start mailing your customers — but it does mean staging needs its own connection or a sandbox.
The neighbouring errors
Two other Google OAuth errors are commonly confused with this one, and both are configuration rather than expiry.
redirect_uri_mismatch — the redirect URI your plugin uses is not listed in your OAuth client’s authorised URIs. It must match exactly, including protocol and any trailing slash. Copy it from the plugin rather than typing it.
unauthorized_client — most often caused by choosing the wrong application type when creating the credentials. You need Web application. Choosing Desktop app produces credentials that fail this way, and it is an easy mistake because “desktop” sounds right for a self-hosted site.
Publishing the app: what actually happens
People hesitate at “Publish app” because it sounds like listing something publicly. It does not.
Publishing changes the consent screen’s status from Testing to In production. Your application is not listed anywhere, not searchable, and not available to anyone who does not have your client credentials. What changes is that refresh tokens stop expiring after seven days.
You may see a warning about verification. Verification is required for applications requesting sensitive or restricted scopes that will be used by people outside your organisation. For a site sending its own mail, one of two things applies:
- Google Workspace users can set the application’s user type to Internal, which limits it to your organisation and skips verification entirely. This is the right answer if you have Workspace.
- Personal Gmail accounts cannot use Internal. You publish to production and may see an unverified-app screen during authorisation, which you can proceed past for your own account.
Neither path requires submitting anything to Google for review in order to send your own mail.
Sending limits you will eventually meet
Independent of tokens, Gmail applies limits designed for people rather than applications — commonly 500 messages a day on a free account and 2,000 on Workspace, with additional per-recipient counting.
The failure is unhelpful: sending stops, and the account may be temporarily restricted for up to 24 hours. On a store during a busy period this arrives as randomly missing order confirmations rather than a clear error.
There is also no delivery log, no bounce handling and no suppression list for application mail. When a customer says they never received a receipt, Gmail gives you nothing to check.
A quick decision
| Your situation | Reasonable choice |
|---|---|
| Brochure site, contact form, a few emails a day | Gmail is fine — publish the app and move on |
| Workspace organisation, internal notifications | Gmail with an Internal app type |
| Store sending order confirmations | A transactional provider — no token expiry, no daily cap, real logs |
| Anything where a missed email costs money | A transactional provider |
Whether Gmail is the right choice here
Worth asking, because a lot of this friction is inherent.
Gmail and Google Workspace are mailbox services. Google applies sending limits designed for humans — commonly 500 messages a day on free Gmail and 2,000 on Workspace — and a busy store will meet them. There is also no delivery log, bounce handling or suppression management for application mail.
Gmail is a reasonable choice for a low-volume site, particularly a brochure site with a contact form. For a store sending order confirmations, a transactional provider will be less work overall — no OAuth app to register, no seven-day expiry, no daily cap, and actual delivery logs.
If you stay on Gmail, NimblyMail stores OAuth access and refresh tokens encrypted at rest rather than as plain text, and its scheduled audit will tell you that the connection has failed rather than leaving you to find out from a customer.
Quick reference
| Symptom | Cause | Fix |
|---|---|---|
Works for exactly 7 days, then invalid_grant |
Consent screen in Testing status | Publish the app to production |
| Stopped abruptly, no pattern | Google account password changed | Reconnect; use a dedicated sending account |
| Stopped after months of little activity | Token unused for 6 months | Reconnect |
| Broke after repeated reconnects | 50-token-per-client limit | Reconnect; stop cycling connections |
redirect_uri_mismatch |
URI not listed, or not an exact match | Paste the plugin’s URI verbatim |
unauthorized_client |
Created as Desktop app | Recreate as Web application |
| Staging cannot send | Token bound to the original site URL | Separate connection, or use a sandbox |
