Just spent two days getting SCIM provisioning working for a custom internal app via Microsoft Graph. The official docs are comprehensive but miss the "why" behind several key steps, especially around the mapping schema. If you're trying to automate user/group provisioning from Entra ID into anything that's not a gallery app, here's the pragmatic path.
First, skip the Azure portal wizard for "non-gallery applications." It's a trap that leads to vague, hard-to-debug errors. You need to define your app's provisioning capability programmatically. The core components are:
1. A service principal for your custom app.
2. A provisioning configuration (`servicePrincipal` resource type) attached to that SP.
3. A SCIM endpoint that adheres to the schema you define.
Here's the critical part: your provisioning schema in Graph must match your SCIM endpoint's exact attribute names. The Graph API won't magically translate `userName` to `email`. If they don't match, provisioning silently fails. Your custom app's SCIM endpoint needs to handle at least `GET /Users` (for delta queries) and `PATCH /Users/{id}`.
Example of creating the provisioning configuration via Graph API (using a service principal ID):
```http
POST https://graph.microsoft.com/v1.0/servicePrincipals/{servicePrincipalId}/synchronization/jobs
Content-Type: application/json
{
"templateId": "scim",
"schedule": {
"expiration": null,
"interval": "PT40M"
}
}
```
After the job is created, you configure the schema. This is where you map Entra ID attributes (like `userPrincipalName`) to your SCIM endpoint's attributes (like `userName`). Use the `synchronizationSchema` endpoint to define this. A common pitfall is not setting the `userName` as the primary matching attribute—it's mandatory for users.
Once configured, you can test by provisioning a single user. Monitor the provisioning logs under the enterprise app in Entra ID; the Graph API audit logs are less helpful for step-by-step debugging. If users get stuck in "not exported" state, 99% of the time it's a schema mismatch or a PATCH request failure from your SCIM endpoint.
Key takeaways:
* Use the Microsoft Graph API directly, not the portal UI, for control.
* Your SCIM endpoint must be robust—handle duplicate `userName` values, return proper HTTP status codes (200, 201, 404).
* The interval is a minimum; changes propagate within that timeframe, not instantly.
* Always start with a test user or group before rolling out to everyone.
Anyone else gone through this? Curious if you used a middleware platform (like Workato) to build the SCIM connector or rolled your own.
Integration is not a project, it's a lifestyle.
Sure, it's a pragmatic path until the delta queries start looping because of a schema mismatch the Graph API "helpfully" ignores. The silent failure on attribute mapping is real, but wait until you see the provisioning cycles chew through API throttling limits when your endpoint's filter logic doesn't match Entra's assumption of `id` vs `externalId`. That's a fun weekend debug session right there.
And skipping the portal wizard is wise, but you've just traded one set of vague errors for another - now all your debugging is in audit logs and provisioning cycle reports. Good luck getting a clear "why" from those.
cost_observer_42
The delta query loop is often a symptom of the endpoint not correctly handling the `meta.version` attribute. Graph expects a version match on subsequent `GET` calls after a `PUT`, and if your app returns a null or mismatched version, it triggers an infinite update cycle.
Your point about audit logs is valid. The provisioning service summaries are notoriously terse. The real debugging happens in the Microsoft Graph change notifications for the `servicePrincipal` object, specifically watching the `provisioningStatusInfo` property. It's the only place you'll see the raw error code from your endpoint before it gets sanitized in the portal.
Throttling from a loop can also impact other apps sharing the same Entra ID tenant, which turns a single-app configuration issue into a platform incident.
—J
Yeah, the silent failure on mismatched attribute names is brutal. I've been burned by that moving from Salesforce to a custom app. The Graph API just gives up and logs a generic "AttributeMappingError" with zero details.
You're right to emphasize the schema, but I'd add a step: test your endpoint with the Microsoft Graph *test connection* API call before you enable full sync. It validates the basic GET and schema, and sometimes gives a clearer error than the provisioning cycles do. It saved me from a similar silent failure loop.
Also, watch out for the 'displayName' attribute. If your custom app's SCIM uses a different field for it, like 'fullName', the mapping will look okay but groups won't provision correctly. Happened to me with Pipedrive's API structure.
Still looking for the perfect one
Silent failure is the optimistic case. Wait until your mapping works perfectly and it starts provisionally deleting users because Graph's default "soft delete" interpretation of a `status` mismatch differs from your app's. The portal wizard is a trap, sure, but hand-rolling the schema via API just gives you a longer leash to hang yourself with.
If you think matching attribute names is the critical part, you haven't hit the real fun: when your endpoint correctly returns `externalId` but your service principal's configuration expects `id`. The logs call it success, but your users pile up as duplicates.
FOSS advocate