Summary and recommendation
GitLab exposes a REST API at `https://gitlab.com/api/v4` with full user lifecycle coverage: create, read, update, block, deactivate, and delete. All write operations and full user detail responses (including email) require a Personal Access Token or OAuth 2.0 token belonging to an admin user - non-admin tokens receive 403 on writes and redacted responses on reads.
User state transitions (block, unblock, activate, deactivate) are not settable via `PUT /users/:id`; each state has a dedicated endpoint (`/block`, `/unblock`, `/activate`, `/deactivate`), and conflating them is a common integration error.
For identity graph construction, the user object exposes `id`, `username`, `email` (admin-only), `state`, `created_at`, and external identity linkages via SCIM's `externalId` field. Pagination uses offset-based `page` and `per_page` params (max 100); the `X-Total` header may be absent on GitLab.com for large result sets, so Link header navigation is the reliable traversal path.
GitLab.com enforces 2,000 authenticated requests per minute at the general API level, with stricter per-endpoint limits on the Users API in some contexts.
API quick reference
| Has user API | Yes |
| Auth method | Personal Access Token (PAT), OAuth 2.0, or Session cookie. PAT is most common for server-to-server; OAuth 2.0 for user-delegated access. |
| Base URL | Official docs |
| SCIM available | Yes |
| SCIM plan required | Premium or Ultimate (GitLab.com groups); requires Group SSO (SAML) to be configured first. |
Authentication
Auth method: Personal Access Token (PAT), OAuth 2.0, or Session cookie. PAT is most common for server-to-server; OAuth 2.0 for user-delegated access.
Setup steps
- Personal Access Token: Navigate to GitLab > User Settings > Access Tokens. Create a token with required scopes (e.g., 'api', 'read_user'). Pass as header: 'PRIVATE-TOKEN:
'. - OAuth 2.0: Register an application at GitLab > User Settings > Applications. Obtain client_id and client_secret. Implement Authorization Code or Client Credentials flow. Pass Bearer token as 'Authorization: Bearer
'. - Admin-level operations (create/delete users) require a PAT or OAuth token belonging to an admin user.
Required scopes
| Scope | Description | Required for |
|---|---|---|
| api | Full read/write access to the authenticated user's API, including all groups and projects. | Creating, updating, deleting users; all write operations. |
| read_user | Read-only access to the authenticated user's profile via /user endpoint. | Reading current user profile only. |
| read_api | Read-only access to the API, including all groups, projects, and user listings. | Listing and reading user data without write access. |
| admin_mode | Allows API calls to be made in admin mode (GitLab 15.8+, self-managed only). | Admin-level user management on self-managed instances. |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | integer | Unique user ID. | auto-assigned | immutable | Used as path param in most endpoints. |
| username | string | Unique username (login handle). | required | optional | Must be unique across instance. |
| string | Primary email address. | required | optional | Must be unique. Admin-only field on read for other users. | |
| name | string | Full display name. | required | optional | |
| password | string | User password. | optional (required if no reset_password) | optional | Write-only; never returned in responses. |
| reset_password | boolean | Send password reset email on creation. | optional | n/a | |
| state | string | User state: active, blocked, deactivated. | auto (active) | via dedicated block/unblock endpoints | Cannot be set directly via PUT; use /block, /unblock, /activate, /deactivate. |
| avatar_url | string | URL to user avatar image. | optional | optional | |
| web_url | string | URL to user's GitLab profile. | auto-assigned | immutable | |
| created_at | datetime (ISO 8601) | Account creation timestamp. | auto-assigned | immutable | |
| bio | string | User biography. | optional | optional | |
| location | string | User location. | optional | optional | |
| public_email | string | Publicly visible email. | optional | optional | |
| skype | string | Skype ID. | optional | optional | |
| string | LinkedIn profile. | optional | optional | ||
| string | Twitter handle. | optional | optional | ||
| admin | boolean | Whether user has admin privileges. | optional | optional | Admin-only field; requires admin token to set. |
| external | boolean | Marks user as external (restricted access). | optional | optional | External users cannot access internal/private projects by default. |
| identities | array | List of external identity providers linked to user. | optional | optional | Each entry has provider and extern_uid fields. |
| two_factor_enabled | boolean | Whether 2FA is enabled. | read-only | read-only | Admin can disable via DELETE /users/:id/two_factor. |
Core endpoints
List users
- Method: GET
- URL:
/api/v4/users - Watch out for: Full user details (including email) are only returned when called by an admin token. Non-admin tokens return a limited public profile.
Request example
GET /api/v4/users?search=john&active=true&per_page=50
PRIVATE-TOKEN: <admin_token>
Response example
[
{
"id": 1,
"username": "john_doe",
"name": "John Doe",
"state": "active",
"email": "john@example.com",
"created_at": "2023-01-15T10:00:00.000Z"
}
]
Get single user
- Method: GET
- URL:
/api/v4/users/:id - Watch out for: Non-admin callers receive a reduced response without email or admin fields.
Request example
GET /api/v4/users/42
PRIVATE-TOKEN: <admin_token>
Response example
{
"id": 42,
"username": "jane_doe",
"name": "Jane Doe",
"email": "jane@example.com",
"state": "active",
"admin": false,
"two_factor_enabled": true
}
Create user
- Method: POST
- URL:
/api/v4/users - Watch out for: Requires admin token. Either password or reset_password:true must be provided. Email confirmation may be required depending on instance settings.
Request example
POST /api/v4/users
PRIVATE-TOKEN: <admin_token>
Content-Type: application/json
{"email":"new@example.com","username":"newuser","name":"New User","reset_password":true}
Response example
{
"id": 99,
"username": "newuser",
"name": "New User",
"email": "new@example.com",
"state": "active",
"created_at": "2024-06-01T12:00:00.000Z"
}
Update user
- Method: PUT
- URL:
/api/v4/users/:id - Watch out for: Requires admin token. Cannot change state via this endpoint; use /block or /deactivate instead.
Request example
PUT /api/v4/users/99
PRIVATE-TOKEN: <admin_token>
Content-Type: application/json
{"name":"Updated Name","email":"updated@example.com"}
Response example
{
"id": 99,
"username": "newuser",
"name": "Updated Name",
"email": "updated@example.com",
"state": "active"
}
Delete user
- Method: DELETE
- URL:
/api/v4/users/:id - Watch out for: hard_delete=true permanently removes the user and all associated records. Default (false) blocks the user and moves contributions to a ghost user. Irreversible if hard_delete=true.
Request example
DELETE /api/v4/users/99?hard_delete=false
PRIVATE-TOKEN: <admin_token>
Response example
HTTP 204 No Content
Block user
- Method: POST
- URL:
/api/v4/users/:id/block - Watch out for: Blocked users cannot log in but their data is preserved. Returns 201 on success, 403 if trying to block an admin, 404 if user not found.
Request example
POST /api/v4/users/99/block
PRIVATE-TOKEN: <admin_token>
Response example
HTTP 201 Created
true
Unblock user
- Method: POST
- URL:
/api/v4/users/:id/unblock - Watch out for: Returns 403 if the user was blocked by LDAP. LDAP-blocked users must be unblocked via LDAP, not the API.
Request example
POST /api/v4/users/99/unblock
PRIVATE-TOKEN: <admin_token>
Response example
HTTP 201 Created
true
Get current authenticated user
- Method: GET
- URL:
/api/v4/user - Watch out for: Returns the profile of the token owner. Useful for token validation. Returns admin fields only if the token owner is an admin.
Request example
GET /api/v4/user
PRIVATE-TOKEN: <token>
Response example
{
"id": 1,
"username": "current_user",
"name": "Current User",
"email": "me@example.com",
"state": "active",
"two_factor_enabled": false
}
Rate limits, pagination, and events
- Rate limits: GitLab.com enforces rate limits per user/IP. Unauthenticated requests are limited more strictly. Authenticated requests have higher limits. Self-managed instances have configurable limits.
- Rate-limit headers: Yes
- Retry-After header: Yes
- Rate-limit notes: Rate limit headers include: RateLimit-Limit, RateLimit-Observed, RateLimit-Remaining, RateLimit-Reset, RateLimit-ResetTime, Retry-After (on 429). Some endpoints (e.g., user creation) may have stricter per-endpoint limits. GitLab.com also enforces 10 req/sec for the Users API specifically in some contexts.
- Pagination method: offset
- Default page size: 20
- Max page size: 100
- Pagination pointer: page and per_page. Response headers include: X-Total, X-Total-Pages, X-Page, X-Next-Page, X-Prev-Page, Link (RFC 5988).
| Plan | Limit | Concurrent |
|---|---|---|
| GitLab.com (authenticated) | 2,000 requests per minute (general REST API) | 0 |
| GitLab.com (unauthenticated) | 500 requests per minute | 0 |
| Self-managed (default) | Configurable; default 2,000 req/min for authenticated users | 0 |
- Webhooks available: Yes
- Webhook notes: GitLab supports system hooks (self-managed, admin-level) and group/project webhooks. System hooks fire on user lifecycle events instance-wide. Group webhooks require Premium or Ultimate.
- Alternative event strategy: Poll GET /api/v4/users with created_after or updated_after query params for change detection on GitLab.com where system hooks are unavailable.
- Webhook events: user_create, user_destroy, user_rename, user_failed_login, member_create, member_update, member_destroy
SCIM API status
SCIM available: Yes
SCIM version: 2.0
Plan required: Premium or Ultimate (GitLab.com groups); requires Group SSO (SAML) to be configured first.
Endpoint: https://gitlab.com/api/scim/v2/groups/:group_path/Users
Supported operations: GET /Users (list provisioned users), GET /Users/:id (get single user), POST /Users (provision/create user), PATCH /Users/:id (update user attributes, including active status for deprovisioning), DELETE /Users/:id (remove user from group)
Limitations:
- SCIM only manages group membership, not instance-level user accounts.
- Requires SAML SSO to be enabled on the group before SCIM can be configured.
- SCIM token is separate from PAT/OAuth tokens; generated in Group > Settings > SAML SSO.
- Supported SCIM attributes are limited: userName, name.formatted, emails[primary], active, externalId.
- DELETE via SCIM removes the user from the group but does not delete the GitLab account.
- Self-managed GitLab uses a different SCIM endpoint path and requires GitLab 11.10+.
- Only one SCIM token per group; rotating it invalidates the previous token immediately.
- Google Workspace is not officially supported as a SCIM provider for GitLab.
Common scenarios
Provisioning a new employee requires two sequential API calls: POST /api/v4/users with reset_password:true to create the account (capture the returned id), then POST /api/v4/groups/:group_id/members with the user_id and numeric access_level to assign group membership.
On instances with email confirmation enabled, the user cannot authenticate until confirmed; pass skip_confirmation:true (admin, self-managed only) to bypass.
Offboarding via the REST API should follow a block-first pattern: POST /users/:id/block immediately revokes login while preserving all data, then enumerate and revoke active PATs via GET /users/:id/personal_access_tokens and DELETE /personal_access_tokens/:token_id. Hard deletion (DELETE /users/:id?hard_delete=true) is irreversible and removes all associated records - confirm data retention policy before use. LDAP-managed users cannot be blocked or unblocked via the API; the change must originate in LDAP.
For IdP-driven lifecycle management, the SCIM 2.0 endpoint (https://gitlab.com/api/scim/v2/groups/:group_path/Users) handles provisioning and deprovisioning against the identity graph at the group level. SCIM requires a dedicated token generated in Group > Settings > SAML SSO - not a PAT or OAuth token. Supported attributes are narrow: userName, name.formatted, emails[primary], active, externalId. A PATCH with active:false removes the user from the group but does not delete the GitLab.com account; the identity record persists and re-provisioning requires re-authentication via SAML.
Provision a new employee and add to a group
- POST /api/v4/users with email, username, name, reset_password:true using an admin PAT.
- Capture the returned user id from the response.
- POST /api/v4/groups/:group_id/members with user_id and access_level (e.g., 30 for Developer) to add the user to the appropriate group.
- Optionally POST /api/v4/projects/:project_id/members to add to specific projects.
Watch out for: If the instance requires email confirmation, the user cannot log in until they confirm. Use skip_confirmation:true (admin only) to bypass on self-managed instances.
Offboard a departing employee (soft delete)
- GET /api/v4/users?search=
to retrieve the user id. - POST /api/v4/users/:id/block to immediately revoke login access while preserving data.
- Optionally DELETE /api/v4/users/:id (without hard_delete) to remove the account and reassign contributions to the ghost user.
- Revoke any active personal access tokens via GET /api/v4/users/:id/personal_access_tokens and DELETE /api/v4/personal_access_tokens/:token_id.
Watch out for: Blocking is reversible; deletion (even soft) is not. Confirm data retention requirements before deleting. LDAP-synced users must be disabled in LDAP first.
Automated SCIM deprovisioning via IdP (Okta/Entra)
- Ensure SAML SSO is configured and active for the GitLab group (Premium or Ultimate required).
- Generate a SCIM token in Group > Settings > SAML SSO > Generate a SCIM token.
- Configure the IdP (Okta, Entra, OneLogin) SCIM connector with base URL https://gitlab.com/api/scim/v2/groups/:group_path and the SCIM token as Bearer auth.
- Map IdP attributes to SCIM fields: userName→username, emails[primary]→email, name.formatted→name, active→active.
- When a user is deactivated in the IdP, the IdP sends PATCH /Users/:id with active:false, removing the user from the GitLab group.
Watch out for: SCIM deprovisions group membership only; it does not delete the GitLab.com account. The user's account remains but loses group access. Re-provisioning requires the user to re-authenticate via SAML.
Why building this yourself is a trap
The most consequential API caveat is the hard_delete flag: DELETE /users/:id?hard_delete=true permanently destroys the user and all owned content with no recovery path. The default soft delete is safer - it blocks the account and reassigns contributions to a ghost user - but is still irreversible.
Any automated offboarding pipeline should gate on an explicit confirmation step before issuing either variant.
The identity graph built from GitLab's API is split across two auth surfaces: REST API tokens (PAT/OAuth) for instance-level user data, and a separate SCIM token for group-level provisioning.
These tokens are not interchangeable, and rotating the SCIM token immediately invalidates the previous one - a silent break for any IdP connector that has not been updated.
On GitLab.com, system hooks (which fire on user lifecycle events instance-wide) are unavailable; the only change-detection alternative is polling GET /api/v4/users with created_after or updated_after query parameters, which adds latency to any event-driven identity graph sync.
Automate GitLab 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.