In this article
May 4, 2026
May 4, 2026

How does SCIM Schema Discovery work

How identity providers learn what your SCIM server can do, through three discovery endpoints.

Explore with AI
Open in ChatGPT
Open in Claude
Open in Perplexity

When an identity provider like Okta or SailPoint connects to a SCIM server for the first time, it doesn’t assume anything about what the server supports. It asks. Three endpoints answer those questions, and the order they get called in matters.

This is a quick walkthrough of what each endpoint does, what clients learn from each one, and why the layered design works the way it does.

What is SCIM?

SCIM (System for Cross-domain Identity Management) is an open standard for automating user provisioning between identity providers (like Okta, Azure AD, SailPoint) and service providers (like us). It defines a REST API and a JSON schema for representing users and groups.

The spec is defined across two RFCs:

  • RFC 7643: Core schema (data model, attribute definitions, extensions)
  • RFC 7644: Protocol (HTTP verbs, filtering, pagination, bulk ops)

Think of it like OIDC’s discovery document (/.well-known/openid-configuration), but split across three endpoints instead of one.

The three discovery endpoints

SCIM has a layered discovery model. A client connects to a SCIM server and needs to figure out three things:

  • what resource types exist
  • what attributes they accept
  • how the server behaves

Three endpoints, consumed in order, answer these questions.

Note that not all IdPs use all three endpoints:

  • Okta: Okta’s provisioning service doesn’t use /ServiceProviderConfig at all, and /Schemas and /ResourceTypes are only available for SCIM 2.0 with entitlements. It also skips bulk operations and POST-based search. In practice, a basic Okta integration skips step 1 and often step 3.
  • Entra: Schema discovery only runs for gallery applications, not custom non-gallery SCIM apps. When it does run, Entra calls /Schemas on save and when reopening the provisioning edit page. Discovered attributes get added to the target attribute mapping, never removed.

Step 1: ServiceProviderConfig, "What can this server do?"

GET /ServiceProviderConfig

This is the handshake. The client reads this first to know how to talk to the server.

Here’s what our server returns:

  
{
    "schemas": [
        "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"
    ],
    "documentationUri": "https://workos.com/docs/integrations/scim",
    "patch": {
        "supported": true
    },
    "bulk": {
        "supported": true,
        "maxOperations": 1000,
        "maxPayloadSize": 1536000
    },
    "filter": {
        "supported": true,
        "maxResults": 100
    },
    "changePassword": {
        "supported": false
    },
    "sort": {
        "supported": false
    },
    "etag": {
        "supported": false
    },
    "authenticationSchemes": [
        {
            "name": "OAuth Bearer Token",
            "description": "Authentication scheme using the OAuth Bearer Token Standard",
            "specUri": "http://www.rfc-editor.org/info/rfc6750",
            "type": "oauthbearertoken",
            "primary": true
        }
    ]
}
  

What the client learns:

  • schemas: Every SCIM response declares its own type. This one is a ServiceProviderConfig.
  • patch.supported: true: The client can use PATCH to partially update users.
  • bulk.supported: true: The client can batch up to 1,000 operations in a single request.
  • filter.supported: true: The client can search users with filters like userName eq "jdoe@example.com".
  • changePassword, sort, etag: All false. Can’t set passwords, can’t sort results, no optimistic concurrency.
  • authenticationSchemes: Use OAuth Bearer Tokens (Authorization: Bearer ... header).

Now the client knows how to talk to the server. Next question: what does it manage?

Step 2: ResourceTypes, "What things does this server manage?"

GET /ResourceTypes

Now the client knows how to talk to the server. Next: what does it manage?

  
{
    "schemas": [
        "urn:ietf:params:scim:api:messages:2.0:ListResponse"
    ],
    "totalResults": 2,
    "itemsPerPage": 2,
    "startIndex": 1,
    "Resources": [
        {
            "schemas": [
                "urn:ietf:params:scim:schemas:core:2.0:ResourceType"
            ],
            "id": "User",
            "name": "User",
            "endpoint": "/Users",
            "description": "User Account",
            "schema": "urn:ietf:params:scim:schemas:core:2.0:User",
            "schemaExtensions": [
                {
                    "schema": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
                    "required": false
                }
            ],
            "meta": {
                "location": ".../ResourceTypes/User",
                "resourceType": "ResourceType"
            }
        },
        {
            "schemas": [
                "urn:ietf:params:scim:schemas:core:2.0:ResourceType"
            ],
            "id": "Group",
            "name": "Group",
            "endpoint": "/Groups",
            "description": "Group",
            "schema": "urn:ietf:params:scim:schemas:core:2.0:Group",
            "meta": {
                "location": ".../ResourceTypes/Group",
                "resourceType": "ResourceType"
            }
        }
    ]
}
  

What the client learns:

This server manages two kinds of resources:

  • The User resource:
    • endpoint: "/Users": Send user CRUD requests here.
    • schema: The core schema is urn:ietf:params:scim:schemas:core:2.0:User (basic fields like userName, emails, etc.)
    • schemaExtensions: The critical part. There’s one extension: the enterprise user schema. required: false means the server accepts enterprise attributes like department, but doesn’t require them.
  • The Group resource:
    • endpoint: "/Groups": Group CRUD goes here.
    • schema: Core group schema, no extensions.

Now the client knows what the server manages and which schemas apply. But it doesn’t yet know the actual attributes within each schema. That’s step 3.

Step 3: Schemas, "What attributes does each schema have?"

GET /Schemas

The client now knows what resource types exist and which schemas they use (from step 2). But those schema URIs are just names. The client doesn’t yet know what attributes each schema contains. That’s what /Schemas resolves: the URIs from step 2 are like foreign keys, and step 3 resolves them to full definitions.

Our server returns three schemas:

  
{
    "totalResults": 3,
    "Resources": [
        { "id": "urn:ietf:params:scim:schemas:core:2.0:User", "name": "User", "attributes": ["...21 attributes..."] },
        { "id": "urn:ietf:params:scim:schemas:core:2.0:Group", "name": "Group", "attributes": ["...2 attributes..."] },
        { "id": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", "name": "EnterpriseUser", "attributes": ["...6 attributes..."] }
    ]
}
  

The one that matters for enterprise attributes like department is the EnterpriseUser extension. Here’s the full schema:

  
{
    "id": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
    "name": "EnterpriseUser",
    "description": "Enterprise User",
    "attributes": [
        {
            "name": "employeeNumber",
            "type": "string",
            "multiValued": false,
            "required": false
        },
        {
            "name": "costCenter",
            "type": "string",
            "multiValued": false,
            "required": false
        },
        {
            "name": "organization",
            "type": "string",
            "multiValued": false,
            "required": false
        },
        {
            "name": "division",
            "type": "string",
            "multiValued": false,
            "required": false
        },
        {
            "name": "department",
            "type": "string",
            "multiValued": false,
            "required": false
        },
        {
            "name": "manager",
            "type": "complex",
            "multiValued": true,
            "subAttributes": [
                { "name": "value", "type": "string" },
                { "name": "$ref", "type": "reference", "referenceTypes": ["User"] }
            ]
        }
    ]
}
  

What the client learns:

  • department: A string, not required, not multi-valued. This is the attribute the customer wanted to send.
  • employeeNumber, costCenter, organization, division: Same pattern, all optional strings.
  • manager: A complex multi-valued attribute with sub-attributes value (the manager’s ID) and $ref (the manager’s URI).

The client can also fetch individual schemas by appending the URI: GET /Schemas/urn:ietf:params:scim:schemas:extension:enterprise:2.0:User

Note that a mature client like SailPoint likely already knows the standard core User and Group schemas (they’re defined in the RFC). The main reason to call /Schemas dynamically is to discover extensions that vary from server to server. If step 2 didn’t reveal any extensions, there’s no reason to call /Schemas at all, which is why SailPoint only hit this endpoint once.

Core schema vs extensions

SCIM defines a core User schema (urn:ietf:params:scim:schemas:core:2.0:User) with basic attributes like userName, name, emails, phoneNumbers, addresses, etc.

Enterprise-specific fields like department, manager, costCenter live in a separate extension schema: urn:ietf:params:scim:schemas:extension:enterprise:2.0:User (defined in RFC 7643 Section 4.3).

When an identity provider sends a user payload, extension attributes are namespaced under the extension URN:

  
{
  "schemas": [
    "urn:ietf:params:scim:schemas:core:2.0:User",
    "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
  ],
  "userName": "jdoe@example.com",
  "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
    "department": "Engineering",
    "costCenter": "4130"
  }
}
  

Build your product, not a SCIM server

The protocol is well-specified. The implementations aren’t. As the variance callout near the top of this guide hinted, every IdP does SCIM a little differently: Okta basically skips discovery, Entra only runs schema discovery for gallery apps, OneLogin and Workday have their own quirks. SCIM also comes in two incompatible major versions (1.1 and 2.0), and some "SCIM-compatible" providers fall back to scheduled CSV uploads in practice.

The real cost of supporting SCIM yourself isn’t implementing the RFCs. It’s implementing the RFCs plus the union of every IdP’s interpretation of them, then keeping up as they change. For a serious enterprise app, that’s a dozen integrations to build and maintain forever, none of which are the thing your customers are actually paying you for.

That’s what WorkOS does. We host the SCIM server, normalize across IdPs, and give your app one consistent API and event stream. You integrate once, support every directory your customers use, and get back to building your product. Get started at workos.com/signup.

Key references

This site uses cookies to improve your experience. Please accept the use of cookies on this site. You can review our cookie policy here and our privacy policy here. If you choose to refuse, functionality of this site will be limited.