Setup guide
Everything you must provision before this skill can run β the exact credentials, permissions, and where to click. Links go to the official consoles and docs, which stay current.
A Workspace super administrator must create a Google Cloud service account, download a JSON key, and authorize that service account's OAuth client ID for domain-wide delegation against an exact list of Admin SDK scopes.
You need to already have
- Super Admin role on the Google Workspace tenant (required to authorize domain-wide delegation and to call the Enterprise License Manager API)
- A Google Cloud project where you hold roles/owner or roles/iam.serviceAccountAdmin plus roles/serviceusage.serviceUsageAdmin
- Service account key creation must not be blocked by the constraints/iam.disableServiceAccountKeyCreation organization policy
- At least one verified domain in the tenant, and a mailbox for the super admin the skill will impersonate
How the pieces connect
Credentials this skill needs
Set these as environment variables. Never paste secrets into a chat or commit them.
| Variable | What it is | Where to get it |
|---|---|---|
GOOGLE_APPLICATION_CREDENTIALSsecret | Service account JSON key file path format: Absolute filesystem path to the downloaded service account key (a JSON document containing client_email, private_key and client_id) | Google Cloud console > IAM & Admin > Service Accounts > click the service account > Keys tab > Add key > Create new key > JSON > Create |
GOOGLE_SERVICE_ACCOUNT_KEYsecretoptional | Service account key, inline format: The full service account JSON key document as a single-line JSON string (use instead of GOOGLE_APPLICATION_CREDENTIALS when writing the key to disk is not acceptable) | Same download as above; paste the file contents into your secret manager rather than saving the file |
GOOGLE_WORKSPACE_ADMIN_EMAIL | Super admin to impersonate (delegation subject) format: Email address in a verified tenant domain, e.g. [email protected] | Google Admin console > Directory > Users > pick a user > Admin roles and privileges β the account must show Super Admin |
GOOGLE_WORKSPACE_CUSTOMER_IDoptional | Customer ID format: The literal string my_customer, or the tenant ID in the form C0xxxxxxx (C followed by 8-9 alphanumerics) | Google Admin console > Account > Account settings > Profile β the Customer ID field. The Enterprise License Manager API will not accept my_customer, so record the real value. |
GOOGLE_WORKSPACE_DOMAIN | Primary domain format: DNS domain name, e.g. example.com | Google Admin console > Account > Domains > Manage domains β the entry labelled Primary |
Permissions to grant
https://www.googleapis.com/auth/admin.directory.userAPI scopeadmin consent requiredCreate, read, patch, suspend, delete and undelete user accounts, move them between org units, and manage aliases.
β³ For audit-only or drift-report runs grant https://www.googleapis.com/auth/admin.directory.user.readonly instead; it cannot mutate the directory.
https://www.googleapis.com/auth/admin.directory.groupAPI scopeadmin consent requiredList and create groups, and read group settings when reconciling a roster against target groups.
β³ Use https://www.googleapis.com/auth/admin.directory.group.readonly when the skill only reports on group membership.
https://www.googleapis.com/auth/admin.directory.group.memberAPI scopeadmin consent requiredCheck membership with groups.hasMember and add or remove members idempotently.
β³ https://www.googleapis.com/auth/admin.directory.group.member.readonly covers the hasMember and members.list checks without allowing writes.
https://www.googleapis.com/auth/admin.directory.orgunitAPI scopeadmin consent requiredList the org unit tree and create missing org units parent-first before moving accounts into them.
β³ https://www.googleapis.com/auth/admin.directory.orgunit.readonly is enough when all target OUs already exist.
https://www.googleapis.com/auth/admin.directory.user.securityAPI scopeadmin consent requiredCall users.signOut to terminate all web and device sessions and reset sign-in cookies during offboarding.
https://www.googleapis.com/auth/admin.directory.customer.readonlyAPI scopeadmin consent requiredRead customers.get to resolve the my_customer alias into the literal C0xxxxxxx customer ID that the Enterprise License Manager API demands.
β³ This is already the read-only variant; the writable https://www.googleapis.com/auth/admin.directory.customer scope is never needed by this skill.
https://www.googleapis.com/auth/apps.licensingAPI scopeadmin consent requiredList licence assignments per SKU, assign licences to new users, move users between SKUs and revoke seats.
β³ https://www.googleapis.com/auth/apps.licensing.user.readonly allows seat reporting without changing any assignment.
Super AdminAdmin console roleadmin consent requiredThe impersonated subject inherits this role; it is what actually authorizes directory writes and licence changes, and it is required to create the domain-wide delegation entry in the first place.
β³ For user and group work only, a delegated account holding the prebuilt User Management Admin plus Groups Admin roles is usually sufficient; the Enterprise License Manager API is only reliably callable as a Super Admin.
Step by step
- 1
Confirm super admin access and record the tenant identifiers
Sign in to the Google Admin console as a super administrator. Go to Account > Account settings > Profile and copy the Customer ID (format C0xxxxxxx). Go to Account > Domains > Manage domains and note the domain marked Primary.
Open in the console / docs β - 2
Create or select a Google Cloud project
In the Google Cloud console create a dedicated project (for example 'workspace-provisioning') or select an existing one. Note the project ID β it appears in quota and SERVICE_DISABLED error messages later.
Open in the console / docs β - 3
Enable the Admin SDK API
Open the API Library entry for the Admin SDK API, confirm the correct project is selected in the project picker, and click Enable.
Open in the console / docs β - 4
Enable the Enterprise License Manager API
Only needed if the skill will assign, move or revoke licences. Open the API Library entry for the Enterprise License Manager API and click Enable.
Open in the console / docs β - 5
Create the service account
Google Cloud console > IAM & Admin > Service Accounts > Create service account. Enter a name, click Create and continue, then click Done. Grant it no Google Cloud IAM roles β its Workspace authority comes from domain-wide delegation, not from IAM.
Open in the console / docs β - 6
Create and download a JSON key
Click the new service account's email > Keys tab > Add key > Create new key > JSON > Create. The file downloads once and cannot be re-downloaded. If this fails, the constraints/iam.disableServiceAccountKeyCreation organization policy is blocking it and needs an exception.
Open in the console / docs β - 7
Copy the service account's numeric OAuth client ID
On the service account's Details tab, expand Advanced settings and copy the Client ID under 'Domain-wide delegation'. It is a 21-digit number and is NOT the client_email β the Admin console will reject an email address here.
Open in the console / docs β - 8
Authorize domain-wide delegation in the Admin console
Google Admin console > Security > Access and data control > API controls > Manage Domain Wide Delegation > Add new. Paste the numeric Client ID, then paste the comma-separated scope list: https://www.googleapis.com/auth/admin.directory.user,https://www.googleapis.com/auth/admin.directory.group,https://www.googleapis.com/auth/admin.directory.group.member,https://www.googleapis.com/auth/admin.directory.orgunit,https://www.googleapis.com/auth/admin.directory.user.security,https://www.googleapis.com/auth/admin.directory.customer.readonly,https://www.googleapis.com/auth/apps.licensing β then click Authorize. Allow a few minutes to propagate.
Open in the console / docs β - 9
Export the environment variables and verify
Set GOOGLE_APPLICATION_CREDENTIALS (or GOOGLE_SERVICE_ACCOUNT_KEY), GOOGLE_WORKSPACE_ADMIN_EMAIL, GOOGLE_WORKSPACE_CUSTOMER_ID and GOOGLE_WORKSPACE_DOMAIN, install the client library with 'pip install google-auth requests', then run the verify command below and expect HTTP 200.
Open in the console / docs β
Check it worked
python -c "import os;from google.oauth2 import service_account as sa;from google.auth.transport.requests import AuthorizedSession;c=sa.Credentials.from_service_account_file(os.environ['GOOGLE_APPLICATION_CREDENTIALS'],scopes=['https://www.googleapis.com/auth/admin.directory.user.readonly']).with_subject(os.environ['GOOGLE_WORKSPACE_ADMIN_EMAIL']);r=AuthorizedSession(c).get('https://admin.googleapis.com/admin/directory/v1/users',params={'customer':'my_customer','maxResults':1});print(r.status_code, r.text[:300])"If you hit an error
unauthorized_client: Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested.Cause: The service account's numeric client ID is not present in Manage Domain Wide Delegation, or the exact scope you requested in code is missing from that entry. The delegation scope list is an allow-list, not a hint.
Fix: At admin.google.com/ac/owl/domainwidedelegation edit the entry for the 21-digit Client ID and paste every scope the code requests, comma-separated with no spaces. Wait a few minutes and retry; propagation can take up to 24 hours.
Not Authorized to access this resource/apiCause: HTTP 403 with reason 'forbidden'. Either no delegation subject was set (the service account is calling as itself), or the impersonated user is not an administrator, or the account lacks the specific privilege for that call.
Fix: Call .with_subject(GOOGLE_WORKSPACE_ADMIN_EMAIL) on the credentials, and confirm at admin.google.com/ac/roles that the impersonated mailbox holds Super Admin (or at least User Management Admin and Groups Admin).
Request had insufficient authentication scopes.Cause: HTTP 403 with status ACCESS_TOKEN_SCOPE_INSUFFICIENT. The minted token does not carry the scope this endpoint requires β commonly calling users.signOut with only admin.directory.user, or a licensing endpoint without apps.licensing.
Fix: Add the exact scope to the SCOPES list in code AND to the Admin console delegation entry, then discard any cached credential object so a new token is minted.
Admin SDK API has not been used in project 123456789012 before or it is disabledCause: HTTP 403 with reason SERVICE_DISABLED. The API is not enabled on the Cloud project that owns the service account.
Fix: Enable admin.googleapis.com (and licensing.googleapis.com for licence work) on that exact project ID, then wait a minute and retry.
invalid_grant: Invalid JWT Signature.Cause: The private key in the JSON file no longer matches a key on the service account (it was deleted or rotated), or the local clock has drifted far enough to invalidate the assertion.
Fix: Generate a fresh JSON key in the Keys tab and replace the credential, and confirm the host clock is NTP-synchronised.
Resource Not Found: userKeyCause: HTTP 404 with reason 'notFound'. The userKey does not resolve β a typo, a user in a different tenant, or a just-created account that has not finished propagating.
Fix: Confirm the address with users.list, and after a create retry the first follow-up call on 404 with backoff for roughly 60 seconds before failing.
Entity already exists.Cause: HTTP 409 with reason 'duplicate' on users.insert or members.insert. The account or membership is already present β usually a re-run of the same batch.
Fix: Treat 409 as convergence rather than failure: fetch the existing resource and fall through to the update path. Idempotent runs should never abort on this.
userRateLimitExceededCause: HTTP 403 (or 429 rateLimitExceeded) after exceeding roughly 2,400 queries per minute per user, more than 10 user creations per second per domain, or more than 1 org-unit write per second per customer.
Fix: Retry with exponential backoff starting at 1 second and doubling to 16 seconds with jitter, and lower write concurrency. The per-account 429 limit cannot be raised in the Cloud console.
Official documentation: https://developers.google.com/workspace/admin/directory/v1/guides/delegation β
API operations this skill uses (20)
Every call the skill makes, linked to its official reference page.
| Operation | Call | Permission | Ref |
|---|---|---|---|
| Resolve the real customer ID Turns the my_customer alias into the C0xxxxxxx value that the Enterprise License Manager API requires. | GET /admin/directory/v1/customers/my_customer | https://www.googleapis.com/auth/admin.directory.customer.readonly | docs β |
| List users (paged) Builds the current-state snapshot that the idempotent diff is computed against. | GET /admin/directory/v1/users?customer=my_customer&maxResults=500&projection=full&viewType=admin_view&query=orgUnitPath='/Sales' | https://www.googleapis.com/auth/admin.directory.user | docs β |
| Get a user Existence check before create, and the source of truth for the field-level delta. | GET /admin/directory/v1/users/{userKey} | https://www.googleapis.com/auth/admin.directory.user | docs β |
| Create a user Onboards a new account with primaryEmail, name, password and changePasswordAtNextLogin. | POST /admin/directory/v1/users | https://www.googleapis.com/auth/admin.directory.user | docs β |
| Patch a user (profile, OU move, suspend or restore) Applies only the changed fields, including orgUnitPath moves and {"suspended": true|false}. Never uses users.update, which clears omitted fields. | PATCH /admin/directory/v1/users/{userKey} | https://www.googleapis.com/auth/admin.directory.user | docs β |
| Sign a user out of all sessions First step of offboarding: terminates web and device sessions and resets sign-in cookies before suspension. | POST /admin/directory/v1/users/{userKey}/signOut | https://www.googleapis.com/auth/admin.directory.user.security | docs β |
| Delete a user Executed only on explicit instruction and after the retention window; the numeric id is recorded first. | DELETE /admin/directory/v1/users/{userKey} | https://www.googleapis.com/auth/admin.directory.user | docs β |
| Undelete a user Recovers an account within the 20-day window; requires the numeric id, not the email. | POST /admin/directory/v1/users/{userId}/undelete | https://www.googleapis.com/auth/admin.directory.user | docs β |
| Add a user alias Attaches firstname.lastname or legacy addresses during onboarding. | POST /admin/directory/v1/users/{userKey}/aliases | https://www.googleapis.com/auth/admin.directory.user | docs β |
| List org units Determines which target OU paths already exist before any account is moved. | GET /admin/directory/v1/customer/{customerId}/orgunits?type=all | https://www.googleapis.com/auth/admin.directory.orgunit | docs β |
| Create an org unit Creates missing paths parent-first, throttled to one write per second per customer. | POST /admin/directory/v1/customer/{customerId}/orgunits | https://www.googleapis.com/auth/admin.directory.orgunit | docs β |
| List groups Resolves target group keys and detects groups that must be created before membership is reconciled. | GET /admin/directory/v1/groups?customer=my_customer&maxResults=200 | https://www.googleapis.com/auth/admin.directory.group | docs β |
| Create a group Provisions a missing distribution or access group referenced by the desired roster. | POST /admin/directory/v1/groups | https://www.googleapis.com/auth/admin.directory.group | docs β |
| Check group membership The idempotence gate before members.insert; unlike members.list it resolves nested membership. | GET /admin/directory/v1/groups/{groupKey}/hasMember/{memberKey} | https://www.googleapis.com/auth/admin.directory.group.member | docs β |
| Add a group member Adds the account with an explicit role of MEMBER, MANAGER or OWNER; 409 is treated as convergence. | POST /admin/directory/v1/groups/{groupKey}/members | https://www.googleapis.com/auth/admin.directory.group.member | docs β |
| Remove a group member Removes memberships that the desired state no longer contains, and part of offboarding. | DELETE /admin/directory/v1/groups/{groupKey}/members/{memberKey} | https://www.googleapis.com/auth/admin.directory.group.member | docs β |
| List licence assignments for a SKU Builds the held-set once per SKU so licence reconciliation does not probe per user. | GET /apps/licensing/v1/product/{productId}/sku/{skuId}/users?customerId={customerId}&maxResults=500 | https://www.googleapis.com/auth/apps.licensing | docs β |
| Assign a licence Gives a newly created account a seat, with body {"userId": "[email protected]"}. | POST /apps/licensing/v1/product/{productId}/sku/{skuId}/user | https://www.googleapis.com/auth/apps.licensing | docs β |
| Move a user to a different SKU Changes edition in place (the path carries the OLD skuId, the body the new one), avoiding the delete-then-insert pattern that surrenders the seat. | PATCH /apps/licensing/v1/product/{productId}/sku/{skuId}/user/{userId} | https://www.googleapis.com/auth/apps.licensing | docs β |
| Revoke a licence Reclaims a seat during offboarding; suspended users keep consuming one until this runs. | DELETE /apps/licensing/v1/product/{productId}/sku/{skuId}/user/{userId} | https://www.googleapis.com/auth/apps.licensing | docs β |