← Google Workspace User Provisioning

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.

Some setupβ‰ˆ 25-40 mingoogle-workspace

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

Create GCP projectconsole.cloud.google.comEnable the APIsAdmin SDK, Reports…Service account+ JSON keyCopy client IDnumeric, not emailAdmin console: authorise the client IDSecurity > API controls > Domain-wide delegationImpersonate an adminsubject = admin@yourdomainThe delegation step happens in the Workspace Admin console, not in Google Cloud β€” this is the step most setups miss.Scopes entered there must match the scopes your code requests exactly, or you get "unauthorized_client".A service account cannot read a user's data without impersonating a real admin via the subject parameter.

Credentials this skill needs

Set these as environment variables. Never paste secrets into a chat or commit them.

VariableWhat it isWhere to get it
GOOGLE_APPLICATION_CREDENTIALS
secret
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_KEY
secretoptional
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_ID
optional
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 required

Create, 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 required

List 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 required

Check 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 required

List 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 required

Call 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 required

Read 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 required

List 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 required

The 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. 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. 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. 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. 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. 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. 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. 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. 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. 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/api

Cause: 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 disabled

Cause: 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: userKey

Cause: 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.

userRateLimitExceeded

Cause: 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.

OperationCallPermissionRef
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_customerhttps://www.googleapis.com/auth/admin.directory.customer.readonlydocs β†—
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.userdocs β†—
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.userdocs β†—
Create a user
Onboards a new account with primaryEmail, name, password and changePasswordAtNextLogin.
POST /admin/directory/v1/usershttps://www.googleapis.com/auth/admin.directory.userdocs β†—
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.userdocs β†—
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}/signOuthttps://www.googleapis.com/auth/admin.directory.user.securitydocs β†—
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.userdocs β†—
Undelete a user
Recovers an account within the 20-day window; requires the numeric id, not the email.
POST /admin/directory/v1/users/{userId}/undeletehttps://www.googleapis.com/auth/admin.directory.userdocs β†—
Add a user alias
Attaches firstname.lastname or legacy addresses during onboarding.
POST /admin/directory/v1/users/{userKey}/aliaseshttps://www.googleapis.com/auth/admin.directory.userdocs β†—
List org units
Determines which target OU paths already exist before any account is moved.
GET /admin/directory/v1/customer/{customerId}/orgunits?type=allhttps://www.googleapis.com/auth/admin.directory.orgunitdocs β†—
Create an org unit
Creates missing paths parent-first, throttled to one write per second per customer.
POST /admin/directory/v1/customer/{customerId}/orgunitshttps://www.googleapis.com/auth/admin.directory.orgunitdocs β†—
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=200https://www.googleapis.com/auth/admin.directory.groupdocs β†—
Create a group
Provisions a missing distribution or access group referenced by the desired roster.
POST /admin/directory/v1/groupshttps://www.googleapis.com/auth/admin.directory.groupdocs β†—
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.memberdocs β†—
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}/membershttps://www.googleapis.com/auth/admin.directory.group.memberdocs β†—
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.memberdocs β†—
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=500https://www.googleapis.com/auth/apps.licensingdocs β†—
Assign a licence
Gives a newly created account a seat, with body {"userId": "[email protected]"}.
POST /apps/licensing/v1/product/{productId}/sku/{skuId}/userhttps://www.googleapis.com/auth/apps.licensingdocs β†—
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.licensingdocs β†—
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.licensingdocs β†—