Summary and recommendation
GitHub exposes user and org management via REST (base URL: https://api.github.com) and a SCIM 2.0 API restricted to Enterprise Managed Users (EMU) on Enterprise Cloud. Auth options are OAuth 2.0, classic or fine-grained PATs, and GitHub App installation tokens.
Key scope requirements: read:org for read-only membership queries, admin:org for write operations on org membership, and admin:enterprise for all SCIM endpoints.
Rate limits are tiered: authenticated OAuth/PAT tokens receive 5,000 requests/hour; GitHub Enterprise Cloud users receive 15,000/hour; SCIM endpoints are capped at approximately 100 requests/minute per enterprise with no documented hard ceiling published by GitHub. All limits are surfaced via X-RateLimit-* response headers; HTTP 429 with a Retry-After header signals primary limit exhaustion.
Secondary abuse-detection limits return HTTP 403 with a separate Retry-After header and are triggered independently of primary limits.
Pagination uses per_page (max 100) and page parameters for most endpoints, with Link headers (rel="next", rel="last") for cursor-style traversal. The SCIM filter parameter uses SCIM 2.0 syntax (e.g., userName eq "user@example.com"), not standard query string syntax.
API quick reference
| Has user API | Yes |
| Auth method | OAuth 2.0 (token-based); Personal Access Token (PAT classic or fine-grained); GitHub App installation token |
| Base URL | Official docs |
| SCIM available | Yes |
| SCIM plan required | GitHub Enterprise Cloud with Enterprise Managed Users (EMU) enabled |
Authentication
Auth method: OAuth 2.0 (token-based); Personal Access Token (PAT classic or fine-grained); GitHub App installation token
Setup steps
- For OAuth 2.0: Register an OAuth App at github.com/settings/developers, obtain client_id and client_secret, redirect user to https://github.com/login/oauth/authorize with required scopes, exchange code for access_token via POST https://github.com/login/oauth/access_token.
- For Personal Access Token (PAT): Navigate to github.com/settings/tokens, generate a classic or fine-grained PAT with required scopes, pass as Authorization: Bearer
or Authorization: token header. - For GitHub App: Create a GitHub App at github.com/settings/apps, install it on the target org/repo, generate a JWT signed with the app's private key, exchange JWT for an installation access token via POST https://api.github.com/app/installations/{installation_id}/access_tokens.
- For SCIM (EMU): Use a personal access token scoped to the enterprise with admin:enterprise scope, or a provisioning token issued by the IdP (Okta, Entra ID). Pass as Authorization: Bearer
.
Required scopes
| Scope | Description | Required for |
|---|---|---|
| read:user | Read-only access to a user's profile data. | GET /user, GET /users/{username} |
| user | Full read/write access to profile information. | PATCH /user (update authenticated user profile) |
| user:email | Read access to a user's email addresses. | GET /user/emails |
| admin:org | Full control of orgs and teams, read and write org projects. | PUT /orgs/{org}/memberships/{username}, DELETE /orgs/{org}/members/{username} |
| read:org | Read-only access to org membership, org projects, and team membership. | GET /orgs/{org}/members, GET /orgs/{org}/memberships/{username} |
| admin:enterprise | Full control of enterprises; required for SCIM provisioning on EMU. | SCIM /scim/v2/enterprises/{enterprise}/Users endpoints |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| login | string | The user's GitHub username. | Set by user during registration; not settable via API. | Not updatable via REST API. | Unique across GitHub. Used as path parameter in most endpoints. |
| id | integer | Unique numeric identifier for the user. | Auto-assigned. | Immutable. | Stable identifier; login can change but id does not. |
| node_id | string | Global node ID for use with GraphQL API. | Auto-assigned. | Immutable. | Base64-encoded GraphQL node identifier. |
| name | string | User's display name. | Optional. | PATCH /user { "name": "..." } | Can be null. |
| string | Publicly visible email address. | Optional. | PATCH /user { "email": "..." } | May be null if user has not set a public email. Use /user/emails for full list. | |
| company | string | User's company name. | Optional. | PATCH /user { "company": "..." } | Can be null. |
| blog | string | URL of user's blog or website. | Optional. | PATCH /user { "blog": "..." } | Can be null. |
| location | string | User's geographic location. | Optional. | PATCH /user { "location": "..." } | Can be null. |
| bio | string | Short biography. | Optional. | PATCH /user { "bio": "..." } | Max 160 characters. |
| hireable | boolean | Whether the user is open to job opportunities. | Optional. | PATCH /user { "hireable": true } | Can be null. |
| twitter_username | string | User's Twitter/X handle. | Optional. | PATCH /user { "twitter_username": "..." } | Can be null. |
| avatar_url | string | URL of the user's avatar image. | Auto-assigned (Gravatar or uploaded). | Not directly updatable via REST. | Read-only via API. |
| type | string | Account type: 'User' or 'Organization' or 'Bot'. | Auto-assigned. | Immutable. | Used to distinguish user vs org accounts. |
| site_admin | boolean | Whether the user is a GitHub site administrator (GitHub Enterprise Server only). | N/A. | Managed via GHES admin API. | Always false on GitHub.com for non-staff. |
| suspended_at | string (ISO 8601) | Timestamp when the user was suspended (EMU/GHES). | N/A. | Set via SCIM or GHES admin API. | Null if not suspended. EMU-specific for GHEC. |
| created_at | string (ISO 8601) | Timestamp of account creation. | Auto-assigned. | Immutable. | Read-only. |
| updated_at | string (ISO 8601) | Timestamp of last profile update. | Auto-assigned. | Auto-updated. | Read-only. |
| role | string | Org membership role: 'admin' or 'member'. | Set when inviting to org. | PUT /orgs/{org}/memberships/{username} { "role": "admin" } | Org-scoped field, not on the base user object. |
| two_factor_authentication | boolean | Whether the user has 2FA enabled. | N/A. | Not settable via API. | Only visible to org admins via GET /orgs/{org}/members?filter=2fa_disabled. |
| public_repos | integer | Number of public repositories owned by the user. | Auto-assigned. | Read-only. | Included in full user object response. |
Core endpoints
Get authenticated user
- Method: GET
- URL:
https://api.github.com/user - Watch out for: Returns only the authenticated user's own profile. Requires at least read:user scope. Email may be null if not public; use GET /user/emails for all addresses.
Request example
GET /user
Authorization: Bearer <token>
Accept: application/vnd.github+json
Response example
{
"login": "octocat",
"id": 1,
"name": "The Octocat",
"email": "octocat@github.com",
"type": "User",
"created_at": "2011-01-25T18:44:36Z"
}
Get a user by username
- Method: GET
- URL:
https://api.github.com/users/{username} - Watch out for: Returns a reduced public profile. Private fields (email, 2FA status) are not exposed unless the requester is an org admin and the user is an org member.
Request example
GET /users/octocat
Authorization: Bearer <token>
Accept: application/vnd.github+json
Response example
{
"login": "octocat",
"id": 1,
"name": "The Octocat",
"public_repos": 8,
"type": "User"
}
Update authenticated user
- Method: PATCH
- URL:
https://api.github.com/user - Watch out for: Only the authenticated user can update their own profile. Requires user scope. Cannot change login or id.
Request example
PATCH /user
Authorization: Bearer <token>
Content-Type: application/json
{"name":"New Name","bio":"Updated bio"}
Response example
{
"login": "octocat",
"name": "New Name",
"bio": "Updated bio",
"updated_at": "2024-01-15T10:00:00Z"
}
List organization members
- Method: GET
- URL:
https://api.github.com/orgs/{org}/members - Watch out for: Returns only public members unless the requester is an org member. Use filter=2fa_disabled to find members without 2FA (requires admin:org scope). Paginate via Link header.
Request example
GET /orgs/my-org/members?per_page=100&page=1
Authorization: Bearer <token>
Accept: application/vnd.github+json
Response example
[
{"login":"user1","id":101,"type":"User"},
{"login":"user2","id":102,"type":"User"}
]
Add or update org membership
- Method: PUT
- URL:
https://api.github.com/orgs/{org}/memberships/{username} - Watch out for: Creates a pending invitation if the user is not already a member. The user must accept the invitation. Requires admin:org scope. Role can be 'member' or 'admin'.
Request example
PUT /orgs/my-org/memberships/newuser
Authorization: Bearer <token>
Content-Type: application/json
{"role":"member"}
Response example
{
"url": "https://api.github.com/orgs/my-org/memberships/newuser",
"state": "pending",
"role": "member",
"user": {"login":"newuser"}
}
Remove org member
- Method: DELETE
- URL:
https://api.github.com/orgs/{org}/members/{username} - Watch out for: Immediately removes the user from the org and all its teams. Requires admin:org scope. Does not delete the GitHub account itself.
Request example
DELETE /orgs/my-org/members/olduser
Authorization: Bearer <token>
Accept: application/vnd.github+json
Response example
HTTP 204 No Content
List org memberships for authenticated user
- Method: GET
- URL:
https://api.github.com/user/memberships/orgs - Watch out for: Requires read:org scope. Filter by state=active|pending. Only returns orgs the authenticated user belongs to.
Request example
GET /user/memberships/orgs?state=active
Authorization: Bearer <token>
Accept: application/vnd.github+json
Response example
[
{
"state": "active",
"role": "admin",
"organization": {"login":"my-org"}
}
]
SCIM: List provisioned users (EMU)
- Method: GET
- URL:
https://api.github.com/scim/v2/enterprises/{enterprise}/Users - Watch out for: Only available for Enterprise Managed Users (EMU) on GitHub Enterprise Cloud. Requires admin:enterprise scoped PAT or IdP provisioning token. Standard SCIM filter syntax supported (e.g., filter=userName eq "user@example.com").
Request example
GET /scim/v2/enterprises/my-enterprise/Users?startIndex=1&count=100
Authorization: Bearer <provisioning-token>
Accept: application/scim+json
Response example
{
"schemas":["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults":2,
"Resources":[
{"id":"abc123","userName":"user@example.com","active":true}
]
}
Rate limits, pagination, and events
- Rate limits: GitHub REST API enforces per-user and per-app rate limits. Authenticated requests are tracked per token. SCIM endpoints have separate, lower limits.
- Rate-limit headers: Yes
- Retry-After header: Yes
- Rate-limit notes: Headers returned: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (Unix timestamp), X-RateLimit-Used, X-RateLimit-Resource. When limit is exceeded, HTTP 429 is returned with Retry-After header. Secondary rate limits (abuse detection) may return 403 with a Retry-After header. Search API has a separate 30 req/min limit for authenticated users.
- Pagination method: cursor
- Default page size: 30
- Max page size: 100
- Pagination pointer: per_page (size), page (offset-based for most endpoints); Link header with rel="next"/"prev"/"last" for cursor-style traversal
| Plan | Limit | Concurrent |
|---|---|---|
| Unauthenticated | 60 requests/hour per originating IP | 0 |
| Authenticated (OAuth / PAT) | 5,000 requests/hour per authenticated user | 0 |
| GitHub App installation token | 5,000 requests/hour; scales to 15,000/hour for orgs with ≥20 users (max 12,500 for the app) | 0 |
| GitHub Enterprise Cloud (GHEC) | 15,000 requests/hour for authenticated users in an enterprise | 0 |
| SCIM API | Approximately 100 requests/minute per enterprise (undocumented hard cap; GitHub recommends throttling provisioning calls) | 0 |
- Webhooks available: Yes
- Webhook notes: GitHub supports organization and enterprise webhooks that fire on user-related events such as membership changes, org invitations, and team membership updates. Webhooks are configured at the org or enterprise level and deliver HTTP POST payloads to a specified URL.
- Alternative event strategy: GitHub REST API polling via GET /orgs/{org}/members or GET /orgs/{org}/audit-log for audit events. GraphQL subscriptions are not available for user-management events.
- Webhook events: membership (added/removed from team), organization (member_added, member_removed, member_invited), org_block (blocked/unblocked), team (added_to_repository, removed_from_repository, created, deleted), enterprise (member_added, member_removed) - enterprise webhooks only
SCIM API status
SCIM available: Yes
SCIM version: 2.0
Plan required: GitHub Enterprise Cloud with Enterprise Managed Users (EMU) enabled
Endpoint: https://api.github.com/scim/v2/enterprises/{enterprise}/Users
Supported operations: GET /scim/v2/enterprises/{enterprise}/Users (list users), GET /scim/v2/enterprises/{enterprise}/Users/{scim_user_id} (get user), POST /scim/v2/enterprises/{enterprise}/Users (provision user), PUT /scim/v2/enterprises/{enterprise}/Users/{scim_user_id} (replace user), PATCH /scim/v2/enterprises/{enterprise}/Users/{scim_user_id} (update user / suspend), DELETE /scim/v2/enterprises/{enterprise}/Users/{scim_user_id} (deprovision user), GET /scim/v2/enterprises/{enterprise}/Groups (list groups/teams), POST /scim/v2/enterprises/{enterprise}/Groups (create group), PATCH /scim/v2/enterprises/{enterprise}/Groups/{scim_group_id} (update group membership), DELETE /scim/v2/enterprises/{enterprise}/Groups/{scim_group_id} (delete group)
Limitations:
- Only available for Enterprise Managed Users (EMU); not available for standard GitHub orgs or GitHub Enterprise Server SCIM via this endpoint.
- SCIM provisioning requires SAML SSO to be configured and enforced via an IdP (Okta or Microsoft Entra ID officially supported).
- Deprovisioning via DELETE suspends the EMU account; the account cannot be reactivated by the user.
- SCIM userName must match the SAML NameID attribute from the IdP.
- Rate limits on SCIM endpoints are lower than standard REST API; GitHub recommends spacing out bulk provisioning calls.
- SCIM Groups map to GitHub Teams within the enterprise; group display names must be unique.
- Google Workspace and OneLogin are not officially supported IdPs for EMU SCIM.
- The enterprise slug (not org name) is required in the SCIM endpoint path.
Common scenarios
Provisioning a new EMU user requires a POST to /scim/v2/enterprises/{enterprise}/Users with a SCIM 2. 0 payload.
The userName field must exactly match the SAML NameID the IdP will assert during SSO; a mismatch causes authentication failures at login. EMU usernames are automatically suffixed with the enterprise slug.
For standard org membership, PUT /orgs/{org}/memberships/{username} creates a pending invitation - the user must accept before they appear as an active member. This async acceptance step breaks any provisioning flow that assumes immediate membership and must be handled with a polling or webhook-based confirmation pattern.
Auditing members missing 2FA is available via GET /orgs/{org}/members?filter=2fa_disabled, but only for org admins using admin:org scope. This filter does not exist at the enterprise level; enterprise-wide 2FA reporting requires the audit log API instead.
For identity graph construction, the user object exposes login (GitHub username), id (stable numeric identifier), node_id (GraphQL global ID), email (public only; use GET /user/emails for all addresses), suspended_at (EMU/GHES suspension timestamp), and type (User, Organization, or Bot). Private fields such as 2FA status are not exposed in the public user object even with elevated scopes.
Provision a new EMU user via SCIM
- Ensure GitHub Enterprise Cloud with EMU is configured and SAML SSO is enforced via Okta or Entra ID.
- Generate a PAT with admin:enterprise scope (or use the IdP's provisioning token).
- POST https://api.github.com/scim/v2/enterprises/{enterprise}/Users with body: {"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"userName":"user@example.com","name":{"givenName":"Jane","familyName":"Doe"},"emails":[{"value":"user@example.com","primary":true}],"active":true}
- GitHub creates the EMU account; the user receives an email to set their password.
- Assign the user to SCIM Groups (GitHub Teams) via PATCH /scim/v2/enterprises/{enterprise}/Groups/{group_id} to grant org/repo access.
Watch out for: The userName must exactly match the SAML NameID the IdP will send during SSO. Mismatch causes authentication failures. EMU usernames are suffixed with the enterprise slug (e.g., jane_doe_myenterprise).
Remove a user from an organization
- Authenticate with a PAT or OAuth token with admin:org scope.
- Verify current membership: GET https://api.github.com/orgs/{org}/memberships/{username}
- Remove the member: DELETE https://api.github.com/orgs/{org}/members/{username}
- Confirm removal: GET /orgs/{org}/members and verify the user is absent.
- For EMU: also PATCH the SCIM user to set active=false to suspend the enterprise account.
Watch out for: DELETE /orgs/{org}/members removes the user from all teams in the org immediately but does not delete their GitHub account or revoke access to repos they have direct collaborator access to outside the org.
Audit org members missing 2FA
- Authenticate with a PAT with admin:org scope.
- GET https://api.github.com/orgs/{org}/members?filter=2fa_disabled&per_page=100
- Paginate through all pages using the Link: <...>; rel="next" header until rel="next" is absent.
- Collect the list of login values for users without 2FA.
- Optionally use PUT /orgs/{org}/memberships/{username} to demote or DELETE /orgs/{org}/members/{username} to remove non-compliant users.
Watch out for: The filter=2fa_disabled parameter only works for org admins. It is not available to regular members. This endpoint does not exist for enterprise-level queries; use the audit log API for enterprise-wide 2FA reporting.
Why building this yourself is a trap
The most significant API trap is the invitation-acceptance gap: PUT /orgs/{org}/memberships/{username} does not provision access - it sends an invitation. Any pipeline that treats a 200 response from this endpoint as confirmation of active membership will produce false positives.
Pair this call with webhook listeners on the organization event (member_added) or poll GET /orgs/{org}/memberships/{username} for state=active.
Fine-grained PATs use different scope syntax than classic PATs and do not yet support all org-level endpoints. Mixing token types across an integration without explicit endpoint compatibility checks will produce unexpected 403 responses.
SCIM is only available for EMU; calling SCIM endpoints against a standard Enterprise Cloud org returns errors. The enterprise slug (not the org name) is required in every SCIM endpoint path - using the org name is a silent misconfiguration that produces 404s.
Additionally, DELETE on a SCIM user suspends the EMU account but does not delete it; the account cannot be reactivated outside the IdP, making deprovisioning effectively irreversible from the GitHub side.
Automate GitHub 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.