ee/api/scim/README.md
SCIM 2.0 (System for Cross-domain Identity Management) enables automated user provisioning and deprovisioning from identity providers (Okta, Azure AD, etc.) into PostHog.
OrganizationDomain model (follows SAML pattern)/scim/v2/{domain_id}/Userspassword=Noneis_email_verified=TrueOrganizationMembership.Level.MEMBERactive=false removes OrganizationMembership onlyUser.is_active=False globallyposthog/models/organization_domain.py - Added scim_enabled, scim_bearer_token fieldsee/api/scim/)auth.py - Bearer token authenticationuser.py - SCIM User adapter (maps to PostHog User model)group.py - SCIM Group adapter (maps to PostHog Role model)views.py - SCIM 2.0 endpointsutils.py - Helper functions for token managementposthog/api/organization_domain.py
scim_enabled)POST /scim/token)ee/urls.py - SCIM URL routingee/settings.py - SCIM service provider configpyproject.toml - Added django-scim2==0.19.0 dependencyee/api/scim/test/test_scim_api.py - Comprehensive SCIM endpoint testsGET /scim/v2/{domain_id}/Users # List users
POST /scim/v2/{domain_id}/Users # Create user
GET /scim/v2/{domain_id}/Users/{id} # Get user
PUT /scim/v2/{domain_id}/Users/{id} # Replace user
PATCH /scim/v2/{domain_id}/Users/{id} # Update user
DELETE /scim/v2/{domain_id}/Users/{id} # Deactivate user
GET /scim/v2/{domain_id}/Groups # List groups
POST /scim/v2/{domain_id}/Groups # Create group
GET /scim/v2/{domain_id}/Groups/{id} # Get group
PUT /scim/v2/{domain_id}/Groups/{id} # Replace group
PATCH /scim/v2/{domain_id}/Groups/{id} # Update group
DELETE /scim/v2/{domain_id}/Groups/{id} # Delete group
GET /scim/v2/{domain_id}/ServiceProviderConfig # Provider capabilities
GET /scim/v2/{domain_id}/ResourceTypes # Resource types
GET /scim/v2/{domain_id}/Schemas # SCIM schemas
PATCH /api/organizations/{org_id}/domains/{domain_id} (scim_enabled) # Enable/disable SCIM
POST /api/organizations/{org_id}/domains/{domain_id}/scim/token # Regenerate bearer token
SCIM configuration (enabled state, base URL) is returned directly on the OrganizationDomain resource.
PATCH: https://app.posthog.com/api/organizations/<org_id>/domains/<domain_id>/
{
"scim_enabled": true
}
Successful response includes the one-time bearer token and SCIM base URL:
{
"id": "<domain_id>",
"domain": "example.com",
"scim_enabled": true,
"scim_base_url": "https://app.posthog.com/scim/v2/<domain_id>",
"scim_bearer_token": "<plain_token_once>",
...
}
PATCH: https://app.posthog.com/api/organizations/<org_id>/domains/<domain_id>/
{
"scim_enabled": false
}
Response mirrors JIT disabling: scim_enabled becomes false and no token is returned.
Authorization: Bearer {token}SCIMBearerTokenAuthentication extracts domain_id from URLOrganizationDomain and validates token (hashed comparison)request.auth for tenant scopingorganization_domain.organizationBoth Users and Groups support standard SCIM PATCH operations via the django-scim2 library.
Replace - Update user attributes:
{
"Operations": [
{ "op": "replace", "path": "name.givenName", "value": "Alice" },
{ "op": "replace", "path": "name.familyName", "value": "Smith" },
{ "op": "replace", "path": "active", "value": false }
]
}
Add - Add/set attributes (reactivate user if adding active=true):
{
"Operations": [{ "op": "add", "path": "name.givenName", "value": "Bob" }]
}
Remove - Clear attributes (deactivates user if removing active):
{
"Operations": [
{ "op": "remove", "path": "name.givenName" },
{ "op": "remove", "path": "active" }
]
}
Replace - Update group name or sync members:
{
"Operations": [
{ "op": "replace", "path": "displayName", "value": "Engineering" },
{ "op": "replace", "path": "members", "value": [{ "value": "user-uuid-1" }, { "value": "user-uuid-2" }] }
]
}
Add - Add members without removing existing ones:
{
"Operations": [{ "op": "add", "path": "members", "value": [{ "value": "user-uuid-3" }] }]
}
Remove - Remove specific members or all members:
{
"Operations": [{ "op": "remove", "path": "members[value eq \"user-uuid\"]" }]
}
SCIM is a licensed feature that requires AvailableFeature.SCIM to be enabled for the organization.
SCIMBearerTokenAuthentication403 ForbiddenEnabling SCIM via Django shell:
from posthog.constants import AvailableFeature
from posthog.models.organization_domain import OrganizationDomain
domain = OrganizationDomain.objects.get(domain="posthog.com")
org = domain.organization
# Add SCIM to available features
org.available_product_features.append({
"key": AvailableFeature.SCIM,
"name": "SCIM"
})
org.save()
Get the bearer token and base URL from Settings → Authentication domains or via Django shell:
token = enable_scim_for_domain(domain)
print(f"Bearer Token: {token}")
scim_url = get_scim_base_url(domain)
print(f"SCIM Base URL: {scim_url}")
POST /scim/v2/{domain_id}/Users
{
"userName": "[email protected]",
"name": {"givenName": "Alice", "familyName": "Smith"},
"active": true
}
Result:
User with password=None, is_email_verified=TrueOrganizationMembership with level=MEMBERIf user exists in another org:
OrganizationMembership to this orgfirst_name, last_name if providedPATCH /scim/v2/{domain_id}/Users/{id}
{
"Operations": [
{"op": "replace", "value": {"name": {"givenName": "Alicia"}}}
]
}
Result: Updates user.first_name = "Alicia"
PATCH /scim/v2/{domain_id}/Users/{id}
{
"Operations": [
{"op": "replace", "value": {"active": false}}
]
}
Result: Deletes OrganizationMembership (user stays active elsewhere)
POST /scim/v2/{domain_id}/Groups
{
"displayName": "Engineering",
"members": [{"value": "user-id"}]
}
Result:
Role with name="Engineering"RoleMembership for specified usersPATCH /scim/v2/{domain_id}/Groups/{id}
{
"Operations": [
{"op": "replace", "value": {"members": [{"value": "user-id-1"}, {"value": "user-id-2"}]}}
]
}
Result: Syncs RoleMembership to match provided list
When both SCIM and JIT (Just-In-Time) provisioning are enabled for a domain:
MEMBER access levelThis allows for a hybrid approach where users can access the organization immediately via SAML, and SCIM handles ongoing attribute and role synchronization from the IdP.
Note: When SCIM provisions a user that already exists (from JIT), it adds them to the organization if they're not already a member, then updates their attributes.
AvailableFeature.SCIM required for accessRun tests:
pytest ee/api/scim/test/test_scim_api.py
pytest ee/api/scim/test/test_users_api.py
pytest ee/api/scim/test/test_groups_api.py
https://app.posthog.com/scim/v2/{domain_id}. For local testing, use your ngrok URL, e.g. https://<ngrok-subdomain>.ngrok.io/scim/v2/{domain_id}. The {domain_id} can be copied directly from the SCIM configuration screen in PostHog.any of the following conditions" and select the roles you want to provision by choosing "Roles include <ONELOGIN-ROLE-NAME>".
Then set the actions to "Map from OneLogin" and "For each roles with a value that matches .*"Note: The custom parameters (email, first_name, last_name) configured in step 5 are NOT sent via SCIM. They are only used in SAML assertions for authentication. SCIM operations use the standard SCIM 2.0 attribute names:
userName for identifieremails[].value array for email addressesname.givenName for first namename.familyName for last nameThe SCIM configuration interface is available in the PostHog settings:
Location: Settings → Organization → Verified Domains → [Domain] → More → Configure SCIM
Features:
AvailableFeature.SCIM is enabledImplementation:
frontend/src/scenes/settings/organization/VerifiedDomains/ConfigureSCIMModal.tsxfrontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.tsPagination:
startIndex and count paramsBulk Operations:
POST /Bulk endpointActivity Logging:
Rate Limiting: