Appearance
SCIM integration
A SCIM integration sends business identities from your provisioning system to Identity Stack. The provisioning system is usually an identity governance system or an HR system. The integration uses the standard SCIM 2.0 protocol (RFC 7643 and RFC 7644).
The provisioning system sends one person at a time, when a value changes. It does not send the complete set on a schedule, as the custom integration does.
A business identity can also name the Microsoft Entra ID user of the person. Identity Stack links the business identity to the user when the user exists.
Authentication
The integration authenticates with an access token from your own Microsoft Entra ID tenant. The integration uses the OAuth 2.0 client credentials flow. No user signs in.
Before you start, make sure that:
- The Identity Stack enterprise application is installed in your tenant. See Setup Identity Stack.
- You can register applications in Microsoft Entra ID.
- You are an administrator in Identity Stack.
1. Register an application
- Go to the Microsoft Entra admin center.
- Go to App registrations and click New registration.
- Enter a name, for example
Identity Stack SCIM. - Select Accounts in this organizational directory only.
- Click Register.
- Write down the Application (client) ID and the Directory (tenant) ID.
2. Add a credential
Go to Certificates & secrets on the application.
- Certificate (recommended). Upload the public key of a certificate. Keep the private key on the server that runs the integration.
- Client secret. Click New client secret. Copy the value immediately. Microsoft Entra ID shows it only one time. Make a new secret before the old secret expires.
Keep the credential in a secure store, for example Azure Key Vault. Do not put it in source code.
3. API permissions
The application does not need an API permission or a role. The SCIM integration that you create in the next step gives the application its access.
Do not give the application the Admin role. The SCIM API does not use the role. The role gives full administrator access to the GraphQL API.
4. Create the SCIM integration in Identity Stack
Create one SCIM integration for each provisioning system. As an administrator, send this mutation to https://api.identitystack.dk/graphql:
graphql
mutation {
createScimIntegration(
input: {
displayName: "Company HR"
clientId: "00000000-0000-0000-0000-000000000001"
}
) {
scimIntegration {
id
enabled
scimEndpoint
}
}
}- Use the Application (client) ID from step 1 as
clientId. - A client ID can be in only one SCIM integration in your organization.
scimEndpointis the base URL for your provisioning system:https://api.identitystack.dk/scim/v2.
A SCIM integration can read and change only the business identities that it created. To stop an integration, use the disableScimIntegration mutation. Identity Stack then refuses the requests of the integration. The business identities of the integration stay in Identity Stack.
5. Get an access token
Request a token from the Microsoft identity platform with the client credentials flow. Replace {tenant-id}, {client-id}, and {client-secret} with your values. URL-encode each value in the request body.
http
POST /{tenant-id}/oauth2/v2.0/token HTTP/1.1
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id={client-id}
&client_secret={client-secret}
&scope=https%3A%2F%2Fapi.identitystack.dk%2F.defaultUse the token until it expires, then get a new token. Most provisioning systems do this for you. Configure OAuth 2.0 client credentials with these values:
| Setting | Value |
|---|---|
| Token URL | https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token |
| Scope | https://api.identitystack.dk/.default |
6. Call the API
Send requests to https://api.identitystack.dk/scim/v2. Put the token in the Authorization header: Bearer {access-token}.
http
GET /scim/v2/ServiceProviderConfig HTTP/1.1
Host: api.identitystack.dk
Authorization: Bearer {access-token}Send request bodies with the content type application/scim+json or application/json. If Identity Stack does not accept the token, see Errors.
The User resource
Each business identity is a SCIM User. A User has the core schema urn:ietf:params:scim:schemas:core:2.0:User. Each User must also have the extension urn:ietf:params:scim:schemas:extension:identitystack:2.0:User.
Core attributes
| Attribute | Required | Description |
|---|---|---|
externalId | Yes | The identifier of the person in your system. Unique in the integration. Case is important. Identity Stack uses it as the key of the person. Keep it the same for the life of the person. |
userName | Yes | Your unique name for the person, for example a user name or an employee number. Unique in the integration. Case is not important. You can change it. Identity Stack does not use it to find the Microsoft Entra ID user. |
name.givenName | Yes | The first name. Maximum 128 characters. |
name.familyName | Yes | The last name. Maximum 128 characters. |
active | No | true if you do not send it. The value false makes the business identity inactive. See Deactivate or delete a person. |
Identity Stack sets id and meta. meta.version is the version of the User. The ETag header also contains the version.
Identity Stack attributes
Put these attributes under the key urn:ietf:params:scim:schemas:extension:identitystack:2.0:User.
| Attribute | Required | Description |
|---|---|---|
affiliationType | Yes | employment, enrollment, engagement, or other |
birthDate | No | YYYY-MM-DD |
nationality | No | A two-letter country code, for example DK |
personalEmail | No | A private email address, not the work address. Maximum 200 characters. |
personalPhone | No | A private phone number, not the work number, in E.164 format. For example +4512345678. |
personalNumbers | No | National personal numbers: type (DkCpr) and value. One number for each type. See Personal numbers and identity documents. |
identityDocumentReferences | No | Passports and national ID cards: type (Passport or IdentityCard), issuingCountry, and documentNumber. See Personal numbers and identity documents. |
entraUserPrincipalName | No | The User Principal Name (UPN) of the Microsoft Entra ID user of the person. See Links to Microsoft Entra ID users. |
entraLinkStatus | Read only | The status of the link to the user. |
entraLinkSource | Read only | asserted when the link comes from entraUserPrincipalName. manual when an administrator made the link. Empty when there is no link. |
entraLinkedUserId | Read only | The object ID of the linked user. Empty when there is no link. |
See Value formats for the format of each value.
Personal numbers and identity documents
These lists contain sensitive data. Identity Stack returns the stored values, as it does for the other attributes. An integration can read only the Users that it created.
You cannot use the lists in a filter. A filter is part of the URL, and many systems write URLs to their logs.
A PUT replaces the full lists. A PATCH can change one item with a path that has a filter. See Change a person.
Example
Request body:
json
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:ietf:params:scim:schemas:extension:identitystack:2.0:User"
],
"externalId": "E-10042",
"userName": "E-10042",
"name": {"givenName": "Anne", "familyName": "Jensen"},
"active": true,
"urn:ietf:params:scim:schemas:extension:identitystack:2.0:User": {
"affiliationType": "employment",
"birthDate": "1990-01-01",
"nationality": "DK",
"personalEmail": "anne@example.net",
"personalPhone": "+4512345678",
"personalNumbers": [{"type": "DkCpr", "value": "010190-1234"}],
"entraUserPrincipalName": "anne.jensen@contoso.dk"
}
}Response body:
json
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:ietf:params:scim:schemas:extension:identitystack:2.0:User"
],
"id": "6f1c2a9e-3b4d-4e8f-9a1b-2c3d4e5f6a7b",
"externalId": "E-10042",
"userName": "E-10042",
"name": {"givenName": "Anne", "familyName": "Jensen"},
"active": true,
"urn:ietf:params:scim:schemas:extension:identitystack:2.0:User": {
"affiliationType": "employment",
"birthDate": "1990-01-01",
"nationality": "DK",
"personalEmail": "anne@example.net",
"personalPhone": "+4512345678",
"personalNumbers": [{"type": "DkCpr", "value": "0101901234"}],
"entraUserPrincipalName": "anne.jensen@contoso.dk",
"entraLinkStatus": "linked",
"entraLinkSource": "asserted",
"entraLinkedUserId": "8a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
},
"meta": {
"resourceType": "User",
"created": "2026-10-20T08:15:00Z",
"lastModified": "2026-10-20T08:15:00Z",
"version": "W/\"1\"",
"location": "https://api.identitystack.dk/scim/v2/Users/6f1c2a9e-3b4d-4e8f-9a1b-2c3d4e5f6a7b"
}
}Operations
All paths are relative to https://api.identitystack.dk/scim/v2. Each operation applies only to the Users of your integration.
Find a person
http
GET /Users?filter=externalId eq "E-10042"- A filter can use
eqonid,userName, andexternalId. - A filter can use
gtorgeonmeta.lastModified. Use it to find the Users that changed, for example a change ofentraLinkStatus. - Other filters return
400with thescimTypeinvalidFilter. - A list contains a maximum of 100 Users on each page. Use
startIndexandcountto get the next page. The first index is 1. The order of the Users does not change between pages. totalResultsis the number of Users that match the filter. Usecount=0to get only this number.
Create a person
http
POST /UsersSend a User in the body, as in the example. Identity Stack returns 201 Created with the User and its id. The Location header contains the URL of the User.
Identity Stack returns 409 with the scimType uniqueness when a different User of the integration has the same userName or externalId.
Do these steps to create a person safely:
- Find the person with a filter on
externalId. - If you find no User, send the
POST. - If the
POSTreturns409, find the person again. A request that timed out can have created the User.
You can create a User with active set to false, for example for a person who starts later.
Change a person
PUT /Users/{id}replaces the User. Identity Stack clears the attributes that you do not send.PATCH /Users/{id}changes only the attributes that you name. Use the SCIM operationsadd,replace, andremove.
Identity Stack returns 200 OK with the User.
A PATCH path can name a sub-attribute or an attribute of the extension. Put the extension key before the attribute name. For example:
name.familyNameurn:ietf:params:scim:schemas:extension:identitystack:2.0:User:affiliationTypeurn:ietf:params:scim:schemas:extension:identitystack:2.0:User:personalNumbers[type eq "DkCpr"].value
json
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{"op": "replace", "path": "name.familyName", "value": "Hansen"},
{
"op": "replace",
"path": "urn:ietf:params:scim:schemas:extension:identitystack:2.0:User:personalPhone",
"value": "+4587654321"
}
]
}Identity Stack ignores read-only attributes in a PUT. A PATCH that changes a read-only attribute returns 400 with the scimType mutability.
To prevent the loss of a newer change, send the last ETag in an If-Match header. If the User changed after that version, Identity Stack returns 412. The ETag is a weak entity tag, for example W/"3". Send the value exactly as you got it.
Deactivate or delete a person
Deactivate. Send a PUT or PATCH that sets active to false. Do this when the person leaves. Identity Stack keeps the business identity and its link to the Microsoft Entra ID user. The business identity is inactive.
- To activate the business identity again, set
activetotrue. For example, do this when the person comes back. - After 90 days as inactive, Identity Stack deletes the personal numbers and the document references of the business identity. The other attributes stay.
Delete. Send DELETE /Users/{id}. Identity Stack deletes the business identity, its personal numbers, its document references, and its link to the user. Identity Stack returns 204 No Content.
A deleted business identity cannot come back. You can create a new User with the same externalId and userName.
Reconcile
A provisioning system sends a change only one time. If a request fails, the two systems can become different. Compare your set of persons with Identity Stack regularly, for example every week:
- Get all Users with
GET /Users, page by page. - Create, change, deactivate, or delete Users until the two sets are the same.
Request formats
Identity Stack accepts these variants, because many provisioning systems send them:
- Attribute names in all cases, for example
UserNameorusername. - PATCH operation names in all cases, for example
Replace. - A PATCH operation without a
path, with an object asvalue. For example{"op": "replace", "value": {"active": false}}. - Boolean values as strings, for example
"False". - Values of
affiliationTypein all cases, for exampleEmployment.
Identity Stack ignores attributes of the core schema that are not in the tables above. It also ignores other schema extensions. Thus a User that you read can have fewer attributes than the User that you sent.
An attribute under the Identity Stack extension that is not in the table returns 400 with the scimType invalidSyntax. Usually the name has an error in it, for example affilationType.
Discovery
GET /ServiceProviderConfig, GET /Schemas, and GET /ResourceTypes show what the API supports.
/ServiceProviderConfigshows support for PATCH, filters, and entity tags./Schemasshows the rules of each attribute: required, mutability, returned, uniqueness, and case.- The API does not support bulk operations, sorting, password changes, or
/Groups.
Errors
The SCIM API uses HTTP status codes. This is different from the GraphQL API. An error has a body with the SCIM error schema:
json
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
"status": "400",
"scimType": "invalidValue",
"detail": "personalNumbers[type eq \"DkCpr\"].value is not a valid CPR number."
}The detail names the attribute that is not correct. It never shows the value.
| Status | scimType | Cause |
|---|---|---|
400 | invalidValue | A required attribute is missing, or a value has the wrong format. |
400 | invalidSyntax | The body is not valid JSON, or it has an attribute under the Identity Stack extension that is not in the table. |
400 | invalidFilter | The filter is not supported. |
400 | mutability | A PATCH changes a read-only attribute. |
401 | The token is missing or expired, or the token is not for the Identity Stack API. | |
403 | The token is valid, but no enabled SCIM integration in your organization has the client ID of the application. Identity Stack also returns 403 for a token of a user. | |
404 | Your integration has no User with this id. Identity Stack also returns 404 for the Users of other integrations. | |
409 | uniqueness | A different User of the integration has the same userName or externalId. |
412 | The User changed after the version in If-Match. | |
429 | You sent too many requests. Wait the number of seconds in the Retry-After header, then send the request again. |
Links to Microsoft Entra ID users
When a User has entraUserPrincipalName, Identity Stack links the business identity to the Microsoft Entra ID user with that UPN. Identity Stack does not use deleted users.
- The link follows the user, not the UPN. When Identity Stack finds the user, it keeps the object ID of the user. If the UPN of the user changes in Microsoft Entra ID later, the link stays.
- Identity Stack finds the user again only when the value changes. If you send a different
entraUserPrincipalName, Identity Stack moves the link. If you removeentraUserPrincipalName, Identity Stack removes the link. - New users. If the user does not exist yet, the status is
pending. Identity Stack looks for the user again in the background. Usually the link is made in less than 15 minutes after the user exists.
entraLinkStatus shows the result:
entraLinkStatus | Description |
|---|---|
linked | The business identity is linked to the user. |
pending | No user has the UPN yet, for example because you create the account later. |
conflict | The user is already linked to a different business identity. |
notAsserted | The User does not have entraUserPrincipalName, and there is no manual link. |
Manual links and conflicts
An administrator can link a business identity manually in Identity Stack.
- If the User does not have
entraUserPrincipalName, Identity Stack keeps the manual link.entraLinkSourceismanual, andentraLinkStatusislinked. - If the User has
entraUserPrincipalName, the value that you send has priority. Identity Stack replaces the manual link and writes the change to the audit log.
A Microsoft Entra ID user can be linked to only one business identity. If the user is already linked to a different business identity, the existing link stays. This applies to business identities from all sources. The status is conflict until an administrator removes the existing link.
Value formats
- CPR number. Use 10 digits, with or without a dash, for example
0101901234or010190-1234. The first six digits must be a valid date (DDMMYY). Identity Stack does not do a modulus 11 check. Many CPR numbers from 2007 and later do not pass the check. Identity Stack returns the number without the dash. - Birth date. Use the format
YYYY-MM-DD. Identity Stack does not compare the birth date with the CPR number. - Phone number. Use the E.164 format: a
+, the country code, and the number, with no spaces. The maximum is 15 digits. For example+4512345678. - Country codes. Use the two-letter code from ISO 3166-1 alpha-2, for example
DK. This applies to the nationality and to the issuing country of a document.