Summary and recommendation
The Admin SDK Directory API exposes full CRUD for Google Workspace users at the base URL https://admin.googleapis.com/admin/directory/v1. Authentication requires OAuth 2.0; server-to-server integrations must use a service account with domain-wide delegation enabled in the Admin console under Security → API Controls.
The required scope for write operations is https://www.googleapis.com/auth/admin.directory.user - read-only workloads should use the narrower .readonly scope. Rate limits default to 1,500 requests per 100 seconds per project; HTTP 429 errors do not include a Retry-After header, so exponential backoff with jitter is mandatory.
For teams building an identity graph across SaaS, the Directory API's stable immutable user ID field is the correct anchor - not primaryEmail, which can be renamed and takes up to 10 minutes to propagate.
API quick reference
| Has user API | Yes |
| Auth method | OAuth 2.0 |
| Base URL | Official docs |
| SCIM available | Yes |
| SCIM plan required | Varies by Workspace edition and supported IdP provisioning flow |
Authentication
Auth method: OAuth 2.0
Setup steps
- Go to Google Cloud Console (console.cloud.google.com) and create or select a project.
- Enable the Admin SDK API under APIs & Services > Library.
- Create OAuth 2.0 credentials (Service Account for server-to-server, or OAuth client ID for user-delegated flows) under APIs & Services > Credentials.
- For Service Accounts: download the JSON key file and enable domain-wide delegation in the Google Workspace Admin Console under Security > API Controls > Domain-wide Delegation.
- Grant the service account the required OAuth scopes in the domain-wide delegation configuration.
- Use the credentials to obtain an access token via Google Auth libraries (e.g., google-auth-library) and include it as a Bearer token in the Authorization header.
Required scopes
| Scope | Description | Required for |
|---|---|---|
| https://www.googleapis.com/auth/admin.directory.user | Full read/write access to user accounts in the domain | Create, update, delete, and list users |
| https://www.googleapis.com/auth/admin.directory.user.readonly | Read-only access to user accounts | List and get user details without modification |
| https://www.googleapis.com/auth/admin.directory.user.security | Manage user security settings (tokens, ASPs, verification codes) | Managing user security credentials |
| https://www.googleapis.com/auth/admin.directory.group | Full read/write access to groups | Managing group membership alongside user operations |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| primaryEmail | string | User primary email address. Use the immutable id as the durable reference key because primaryEmail can be renamed. | required | allowed | Changing primaryEmail is supported through users.update, but rename propagation is not instantaneous across all services. |
| name.givenName | string | User's first name | required | optional | Max 60 characters |
| name.familyName | string | User's last name | required | optional | Max 60 characters |
| password | string | User's password (hashed or plaintext on input) | required | optional | Min 8 characters. Can specify hashFunction (MD5, SHA-1, crypt) alongside hashed value. |
| id | string | Immutable unique identifier for the user | server-assigned | read-only | Use as stable identifier; primaryEmail can change. |
| suspended | boolean | Whether the user account is suspended | optional | optional | Suspended users cannot sign in but data is retained. |
| orgUnitPath | string | Organizational unit path the user belongs to | optional | optional | Defaults to root OU if not specified. |
| isAdmin | boolean | Whether the user has super admin privileges | optional | optional | Use makeAdmin method to promote; cannot set directly via users.update. |
| emails | array | List of email addresses for the user | optional | optional | Includes aliases and alternate addresses. |
| phones | array | List of phone numbers for the user | optional | optional | Each entry has 'value' and 'type' fields. |
| addresses | array | List of physical addresses | optional | optional | Structured with streetAddress, city, country, etc. |
| organizations | array | User's organization/department/title information | optional | optional | Includes title, department, costCenter fields. |
| externalIds | array | External identifiers (e.g., employee ID) | optional | optional | Use type='organization' for employee ID. |
| recoveryEmail | string | Recovery email address for the user | optional | optional | Used for account recovery flows. |
| recoveryPhone | string | Recovery phone number in E.164 format | optional | optional | Must include country code (e.g., +15551234567). |
| changePasswordAtNextLogin | boolean | Forces password change on next login | optional | optional | Useful for initial provisioning flows. |
| includeInGlobalAddressList | boolean | Whether user appears in the Global Address List | optional | optional | Defaults to true. |
| lastLoginTime | string (datetime) | Timestamp of user's last login | read-only | read-only | ISO 8601 format. May be empty if user never logged in. |
| creationTime | string (datetime) | Timestamp when the user account was created | server-assigned | read-only | ISO 8601 format. |
| customSchemas | object | Custom attribute schemas defined for the domain | optional | optional | Must define schema first via Admin Console or Schemas API. |
Core endpoints
List Users
- Method: GET
- URL:
https://admin.googleapis.com/admin/directory/v1/users?domain={domain}&maxResults=100 - Watch out for: Must specify either 'domain' or 'customer' (use 'my_customer' for the authenticated admin's domain). Without one, the request returns a 400 error.
Request example
GET /admin/directory/v1/users?domain=example.com&maxResults=100
Authorization: Bearer {access_token}
Response example
{
"kind": "admin#directory#users",
"nextPageToken": "token123",
"users": [
{"id": "123", "primaryEmail": "alice@example.com", "name": {"fullName": "Alice Smith"}}
]
}
Get User
- Method: GET
- URL:
https://admin.googleapis.com/admin/directory/v1/users/{userKey} - Watch out for: userKey can be the user's primary email, alias email, or immutable user ID. Using the immutable ID is safest for long-lived references.
Request example
GET /admin/directory/v1/users/alice@example.com
Authorization: Bearer {access_token}
Response example
{
"id": "118...",
"primaryEmail": "alice@example.com",
"name": {"givenName": "Alice", "familyName": "Smith"},
"suspended": false,
"orgUnitPath": "/Engineering"
}
Create User
- Method: POST
- URL:
https://admin.googleapis.com/admin/directory/v1/users - Watch out for: New users may take up to 24 hours to appear in all Google services. Password must meet domain password policy requirements.
Request example
POST /admin/directory/v1/users
Authorization: Bearer {access_token}
Content-Type: application/json
{
"primaryEmail": "bob@example.com",
"name": {"givenName": "Bob", "familyName": "Jones"},
"password": "SecurePass123!"
}
Response example
{
"id": "119...",
"primaryEmail": "bob@example.com",
"name": {"givenName": "Bob", "familyName": "Jones"},
"creationTime": "2025-01-15T10:00:00.000Z"
}
Update User
- Method: PUT
- URL:
https://admin.googleapis.com/admin/directory/v1/users/{userKey} - Watch out for: PUT replaces the full resource. Use PATCH for partial updates to avoid accidentally clearing fields not included in the request body.
Request example
PUT /admin/directory/v1/users/bob@example.com
Authorization: Bearer {access_token}
Content-Type: application/json
{
"name": {"givenName": "Robert", "familyName": "Jones"},
"orgUnitPath": "/Sales"
}
Response example
{
"id": "119...",
"primaryEmail": "bob@example.com",
"name": {"givenName": "Robert", "familyName": "Jones"},
"orgUnitPath": "/Sales"
}
Patch User (partial update)
- Method: PATCH
- URL:
https://admin.googleapis.com/admin/directory/v1/users/{userKey} - Watch out for: Preferred over PUT for targeted field updates. Only fields included in the request body are modified.
Request example
PATCH /admin/directory/v1/users/bob@example.com
Authorization: Bearer {access_token}
Content-Type: application/json
{
"suspended": true
}
Response example
{
"id": "119...",
"primaryEmail": "bob@example.com",
"suspended": true
}
Delete User
- Method: DELETE
- URL:
https://admin.googleapis.com/admin/directory/v1/users/{userKey} - Watch out for: Deleted users remain recoverable for up to 20 days. Transfer data before deletion and use the immutable user ID for undelete workflows.
Request example
DELETE /admin/directory/v1/users/bob@example.com
Authorization: Bearer {access_token}
Response example
HTTP 204 No Content
Make User Admin
- Method: POST
- URL:
https://admin.googleapis.com/admin/directory/v1/users/{userKey}/makeAdmin - Watch out for: Cannot set isAdmin directly via users.insert or users.update; must use this dedicated makeAdmin method.
Request example
POST /admin/directory/v1/users/alice@example.com/makeAdmin
Authorization: Bearer {access_token}
Content-Type: application/json
{
"status": true
}
Response example
HTTP 204 No Content
Undelete User
- Method: POST
- URL:
https://admin.googleapis.com/admin/directory/v1/users/{userKey}/undelete - Watch out for: Use the immutable user ID for deleted users. Google documents restore for up to 20 days after deletion.
Request example
POST /admin/directory/v1/users/119.../undelete
Authorization: Bearer {access_token}
Content-Type: application/json
{
"orgUnitPath": "/"
}
Response example
HTTP 204 No Content
Rate limits, pagination, and events
- Rate limits: Admin SDK Directory API enforces per-domain and per-project quotas. Default is 1,500 queries per 100 seconds per project, and 1,500 queries per 100 seconds per user. Burst limits apply.
- Rate-limit headers: No
- Retry-After header: No
- Rate-limit notes: Quota increases can be requested via Google Cloud Console. Errors return HTTP 429 with a 'userRateLimitExceeded' or 'rateLimitExceeded' reason. Implement exponential backoff. Quotas are visible in Cloud Console under APIs & Services > Quotas.
- Pagination method: token
- Default page size: 100
- Max page size: 500
- Pagination pointer: pageToken
| Plan | Limit | Concurrent |
|---|---|---|
| All Google Workspace plans | 1,500 requests per 100 seconds per project; 1,500 requests per 100 seconds per user | 0 |
- Webhooks available: Yes
- Webhook notes: The Admin SDK Directory API supports push notifications (webhooks) via the watch method. Clients register a HTTPS endpoint to receive notifications when resources change. Channels expire and must be renewed (max TTL is 604800 seconds / 7 days).
- Alternative event strategy: Google Workspace Admin Reports API (activities.list) provides audit log polling for admin, login, and user events as an alternative to push notifications.
- Webhook events: users.watch – changes to user resources, groups.watch – changes to group resources, members.watch – changes to group membership, orgunits.watch – changes to organizational units
SCIM API status
SCIM available: Yes
SCIM version: 2.0
Plan required: Varies by Workspace edition and supported IdP provisioning flow
Endpoint: Varies by IdP integration. Google Workspace acts as a SCIM service provider when provisioned from Okta, Azure AD/Entra ID, or OneLogin. The SCIM endpoint URL is provided within the IdP's Google Workspace app configuration (e.g., Okta generates it automatically). Google does not publish a standalone generic SCIM base URL; provisioning is configured through supported IdP connectors.
Supported operations: Create users (POST /Users), Read users (GET /Users, GET /Users/{id}), Update users (PUT /Users/{id}, PATCH /Users/{id}), Deactivate/suspend users (PATCH active=false), List users with filtering (GET /Users?filter=), Push groups/manage group membership (POST /Groups, PATCH /Groups/{id})
Limitations:
- Google Workspace is a SCIM destination (service provider), not a SCIM source; it cannot push SCIM events to external systems.
- SCIM provisioning requires SSO to be configured first via SAML.
- Not all Directory API user fields are exposed via SCIM; custom schemas are not supported through SCIM.
- SCIM group push support varies by IdP connector.
- Google does not expose a generic public SCIM endpoint; provisioning is only available through certified IdP integrations (Okta, Azure AD/Entra ID, OneLogin, Ping Identity, etc.).
Common scenarios
Three scenarios cover the majority of programmatic use cases. First, provisioning: POST /users with primaryEmail, name fields, password, orgUnitPath, and changePasswordAtNextLogin: true; capture the returned immutable id immediately for all subsequent references.
Note that new accounts can take up to 24 hours to be fully active across all Google services - do not assume immediate downstream availability. Second, offboarding: PATCH /users/{userKey} with suspended: true to block sign-in while preserving data, then invoke the Data Transfer API before issuing DELETE.
deleting without suspending first forfeits the window to transfer data while the account is still accessible. Third, bulk audit: GET /users?
customer=my_customer&maxResults=500&projection=full with pageToken pagination (max page size 500); collect lastLoginTime and suspended fields, then cross-reference with the Reports API activities endpoint for login detail.
Use projection=basic when only a subset of fields is needed - full projection increases payload size significantly at scale.
Provision a new employee and assign to an OU
- POST /admin/directory/v1/users with primaryEmail, name.givenName, name.familyName, password, orgUnitPath, and changePasswordAtNextLogin: true.
- Capture the returned immutable 'id' field for future stable references.
- PATCH /admin/directory/v1/users/{id} to add phone, organizations (title, department), and externalIds (employee ID) if not set at creation.
- If the user needs group membership, POST /admin/directory/v1/groups/{groupKey}/members with the user's email.
Watch out for: New accounts may take up to 24 hours to be fully active across all Google services. Do not assume immediate availability for downstream integrations.
Suspend and offboard a departing employee
- PATCH /admin/directory/v1/users/{userKey} with suspended: true to immediately block sign-in while preserving data.
- Use the Data Transfer API (POST /admin/datatransfer/v1/transfers) to transfer Drive, Calendar, and other data to a manager or archive account.
- Remove the user from groups via DELETE /admin/directory/v1/groups/{groupKey}/members/{memberKey}.
- After data transfer is confirmed, DELETE /admin/directory/v1/users/{userKey} to initiate the 20-day soft-delete window.
Watch out for: Deleting without suspending first increases offboarding risk. Suspend first, transfer data, then rely on the documented 20-day recovery window only as a safety net.
Bulk list and audit all users in a domain
- GET /admin/directory/v1/users?customer=my_customer&maxResults=500&projection=full to retrieve the first page of users with all fields.
- Check the response for 'nextPageToken'; if present, repeat the request with pageToken={nextPageToken} until no token is returned.
- Collect lastLoginTime and suspended fields to identify inactive or suspended accounts.
- Cross-reference with the Reports API (GET /admin/reports/v1/activity/users/all/applications/login) for detailed login activity.
Watch out for: Using projection=full significantly increases response payload size and may slow down pagination for large domains. Use projection=basic and specific fields parameter if only a subset of fields is needed.
Why building this yourself is a trap
Three API behaviors matter in production. First, isAdmin cannot be set through users.insert or users.update - you need the dedicated makeAdmin method. Second, deleted users remain recoverable for up to 20 days, and undelete should use the immutable user ID rather than the email address.
Third, watch-based event delivery is not set-and-forget; channels expire and need explicit renewal, so long-running integrations need renewal logic and a fallback polling plan.
Automate Google Workspace workflows without one-off scripts
Stitchflow builds and maintains end-to-end IT automation across your SaaS stack, including apps without APIs. Built for exactly how your company works, with human approvals where they matter.