The console
Create a project
charter will do — and Create.Already have one? Pick it from the project picker at the top of the console
instead. Everything below happens inside whichever project is selected there,
so check it before each step.
Turn on the APIs you will call

403 with “Gmail API has not been used
in project N before or it is disabled”. It reads like a permission problem and
is not one.Set up the consent screen

- App information — an app name, which is what the consent screen will show you, and a support email, which can be your own.
- Audience — External. Internal is offered only inside a Google Workspace organisation; if you see it and every account you will connect belongs to that organisation, choose it, and step 6 does not apply to you.
- Contact information — your email again.
- Finish — agree to the policy, then Create.


Create the OAuth client

- Just you: Desktop app
- Your users: Web application
localhost on any port, which is what
lets the script in step 7 catch Google’s answer with nothing registered.
Download the client JSON before you click OK
client_id and client_secret, under installed — and step 7
needs both.

Publish the app

invalid_grant-on-Monday symptom, which is this setting rather than a bug in
your storage. It also refuses consent to anyone not listed under Test users.Published, consent shows “Google hasn’t verified this app”: click
Advanced, then Go to your app’s name. For your own account that warning
is the whole cost. Verification — and for a restricted scope like
gmail.modify, a security assessment — is what letting other people connect
eventually needs, and it takes weeks;
check Google’s current rules
before planning around it.Rather stay in Testing? Then Add users with your own address, and expect to
repeat step 7 every week.The grant
Connect your account
examples/connect_google.py
runs the consent flow once, from a terminal. Give it the two values from the
file in step 5:403.
The script names any you dropped. The tab says Connected, and the terminal
prints the third value:main() to ask for less. The scopes
are listed on Google’s provider page, and
scopes_for is the call that reads them off the
tools.The script is not a library feature. It is a page you can read, and only two of
its calls are Charter’s: flow.authorize() builds the URL with PKCE and a
state, and flow.exchange() trades the code for tokens. Everything between
them — the redirect, the wait, the callback — is the script’s, and in a web app
it would be your framework’s: getting the grant.Store the three values
export lines — the two you typed and the one printed — in your
shell profile, a .env file, or your secret manager. That is the whole
configuration: every Google pack, and the MCP server, renews an
access token from them on its own.Not the access token. It lasts about an hour, and the client fetches a fresh one
whenever it needs one.Google does not rotate refresh tokens, so the value printed is the value you
keep — a refresh returns a new access token and the same refresh token. Slack
and GitHub do rotate, and a server that does needs the on_refresh hook to
write the new one back, which is
what varies between servers.To build the client yourself instead, an OAuth2Client
turns those three values into a credential provider: it fetches an access
token, caches it, and renews it before it lapses.Verify
403 from the API, hours later, inside an agent. One call settles it now:INBOX, SENT, and whatever else you have made. It costs 1 unit of Gmail’s
quota, the cheapest call in the pack.That is the loop closed: a client you registered, a grant you own, a token the
runtime renews without being asked, and a response that came back. Hand
gmail.TOOLS to an adapter and the same credential serves
every tool in all six packs.When it does not work
403: “… API has not been used in project N before or it is disabled”
403: “… API has not been used in project N before or it is disabled”
“Access blocked: … has not completed the Google verification process”, or access_denied
“Access blocked: … has not completed the Google verification process”, or access_denied
invalid_grant, about a week after it last worked
invalid_grant, about a week after it last worked
redirect_uri_mismatch
redirect_uri_mismatch
403: “Request had insufficient authentication scopes”
403: “Request had insufficient authentication scopes”
“Google hasn't verified this app”
“Google hasn't verified this app”
Your users’ accounts
The console steps are the same, with a Web application client (step 4’s second tab). What changes is everything after: the consent route lives in your app, each user’s grant is stored against them, and many users means verification. Getting the grant is the route; serving many users is the per-user cache.What this does not cover
Named rather than left for you to discover:- Rotation. Google hands back the same refresh token; Slack and GitHub do not. What varies between servers.
- Revocation. A grant killed at
myaccount.google.com/permissionsanswersinvalid_grant, and Charter then stops asking for 60 seconds rather than hammering a rate-limited endpoint — refresh behaviour. - The rest of Google’s silent failures — the hundred-refresh-token cap, what
login_hintdoes and does not guarantee — what fails silently.
Related
- Google — the server constant, every scope, the hundred-token cap and what else fails silently
- Getting the grant — the same two protocol steps, inside your app
- Authorization servers — refresh, caching, many users
- Every pack — what the other packs need, in one table
- Credentials — every credential provider, as reference
