domainwidedelegation.comGet it set up

How to set up domain-wide delegation

Five steps. The first four are quick; the fifth is the one people skip, and skipping it is why delegation appears to work in testing and fails in production three weeks later.

Before you start

Step 1 — Create the service account

In the Cloud console, create a service account in your project and generate a JSON key. Store that key like a password: anyone holding it can act as any user you have authorized scopes for. Then enable each API you intend to call in the same project — a missing API enablement produces a different, less obvious error than a missing scope.

Step 2 — Copy the client ID, not the email

Open the service account and copy its OAuth 2 client ID — a long numeric string. The Admin console keys the grant to this number. Pasting the service account email address instead is a common false start.

Step 3 — Authorize the scopes

In the Workspace Admin console, go to Security → Access and data control → API controls → Manage domain-wide delegation, add the client ID, and paste your scopes as a comma-separated list.

Exact-match warning. Scopes are compared as strings. https://www.googleapis.com/auth/gmail.readonly and https://mail.google.com/ are different grants. Authorizing one does not imply the other, and requesting an unauthorized one fails the whole request — not just that API.

Step 4 — Impersonate in code

The pattern is the same in every language: load the key, attach scopes, then set the subject to the user you are acting as.

from google.oauth2 import service_account
from googleapiclient.discovery import build

SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]

creds = service_account.Credentials.from_service_account_file(
    "/secure/path/key.json", scopes=SCOPES
).with_subject("person@yourdomain.com")   # the user you are acting as

gmail = build("gmail", "v1", credentials=creds)
print(gmail.users().getProfile(userId="me").execute())

The with_subject call is the delegation. Without it you are the service account itself — which owns no mailbox, no calendar and no Drive storage.

Step 5 — Verify each API, not just one

Refresh a token and make one cheap read per API. This is the step that catches a scope you forgot to authorize, an API you forgot to enable, and a tenant where the grant never propagated. A green Gmail check tells you nothing about Drive.

for api, probe in [
    ("gmail",    lambda c: build("gmail","v1",credentials=c).users().getProfile(userId="me").execute()),
    ("drive",    lambda c: build("drive","v3",credentials=c).about().get(fields="storageQuota").execute()),
    ("calendar", lambda c: build("calendar","v3",credentials=c).calendarList().list().execute()),
]:
    try:
        probe(creds); print(api, "OK")
    except Exception as e:
        print(api, "FAILED:", repr(e)[:160])

Two things worth doing on day one

Stuck at step 3 or 5?

Those are the two that consume the day. We do this setup as a service, including multi-tenant grants and a verification pass that proves every API answers before we hand it over. $500 per hour.

Get it set up properly