Inbound API-Driven Provisioning

API-driven inbound provisioning lets external systems push identity data into Microsoft Entra ID (or on-premises Active Directory) through a Graph API endpoint. Unlike outbound provisioning where Entra pulls or pushes to target applications, inbound API provisioning reverses the direction: an external system of record sends data to Entra ID.

This is the integration path when the identity source is not Workday or SAP SuccessFactors (which have purpose-built gallery integrations), but rather a custom HR system, payroll platform, student information system, or any data source that can make HTTP calls.

Architecture and Data Flow

The inbound API provisioning architecture has four layers:

  1. System of record. The authoritative source of identity data (HR system, payroll, SIS, CSV export, SQL database, LDAP directory, or any custom system).
  2. API client. An automation component that reads data from the system of record and sends it to the Entra provisioning service. This can be a PowerShell script, Azure Logic App, Azure Function, or any HTTP client.
  3. Entra provisioning service. The cloud engine that processes incoming data, applies attribute mappings, scoping filters, and matching rules, then provisions users to the configured target.
  4. Target directory. Either Microsoft Entra ID (cloud-only users) or on-premises Active Directory (hybrid users via the provisioning agent).

The API client does not need to determine whether to create, update, or disable users. It uploads the current state from the source system; the provisioning engine handles comparison with existing user records and determines the appropriate action (create, update, disable, or no-op).

sequenceDiagram
    participant SoR as System of Record
    participant Client as API Client
    participant Graph as Microsoft Graph API
    participant Prov as Provisioning Service
    participant Target as Entra ID / On-Prem AD

    Client->>SoR: Read identity data
    SoR-->>Client: User records
    Client->>Graph: POST /bulkUpload (SCIM payload)
    Graph-->>Client: 202 Accepted
    Graph->>Prov: Queue bulk request
    Prov->>Prov: Apply scoping, mapping, matching
    Prov->>Target: Create / Update / Disable users
    Client->>Graph: GET /auditLogs/provisioning
    Graph-->>Client: Provisioning log entries

The following diagram shows where inbound API provisioning fits within the broader Entra provisioning platform:

architecture-beta
    group platform(cloud)[Entra Provisioning Platform]

    service inbound(server)[Inbound API] in platform
    service outbound(server)[Outbound SCIM] in platform
    service engine(server)[Provisioning Engine] in platform

    group sources(cloud)[Identity Sources]
    service hr(server)[HR Systems] in sources
    service custom(server)[Custom Sources] in sources

    group targets(cloud)[Targets]
    service entra(server)[Entra ID] in targets
    service ad(server)[On-Prem AD] in targets
    service saas(server)[SaaS Apps] in targets

    hr:R --> L:inbound
    custom:R --> L:inbound
    inbound:R --> L:engine
    engine:R --> L:entra
    engine:R --> L:ad
    outbound:R --> L:saas
    engine:B --> T:outbound

API Surface

Endpoint

POST https://graph.microsoft.com/beta/servicePrincipals/{servicePrincipalId}/synchronization/jobs/{jobId}/bulkUpload

The servicePrincipalId and jobId are specific to the inbound provisioning app you create in the Entra admin center. You can retrieve them via the Graph API:

GET https://graph.microsoft.com/beta/servicePrincipals?$filter=displayName eq '{appName}'
GET https://graph.microsoft.com/beta/servicePrincipals/{id}/synchronization/jobs

Request Format

The request body uses SCIM bulk request format with Content-Type: application/scim+json:

{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:BulkRequest"],
  "Operations": [
    {
      "method": "POST",
      "bulkId": "unique-id-1",
      "path": "/Users",
      "data": {
        "schemas": [
          "urn:ietf:params:scim:schemas:core:2.0:User",
          "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
        ],
        "externalId": "EMP001",
        "userName": "jdoe@contoso.com",
        "active": true,
        "displayName": "Jane Doe",
        "name": {
          "givenName": "Jane",
          "familyName": "Doe"
        },
        "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
          "employeeNumber": "EMP001",
          "department": "Engineering",
          "manager": {
            "value": "MGR001"
          }
        }
      }
    }
  ]
}

Key Constraints

ConstraintValue
Max operations per request50 user records
Throttle rate40 calls per 5-second window
Daily call limit (P1/P2 license)2,000 calls per 24 hours
Daily call limit (Governance license)6,000 calls per 24 hours
Max users per day (P1/P2)100,000 (2,000 calls x 50 records)
Max users per day (Governance)300,000 (6,000 calls x 50 records)
Response202 Accepted (asynchronous processing)

SCIM Schema

The standard schemas are:

  • urn:ietf:params:scim:schemas:core:2.0:User - core user attributes (userName, displayName, name, emails, active)
  • urn:ietf:params:scim:schemas:extension:enterprise:2.0:User - enterprise attributes (employeeNumber, department, manager, costCenter, division, organization)

For attributes not covered by the standard schemas, you can define custom SCIM schema extensions:

  • urn:ietf:params:scim:schemas:extension:<company>:1.0:User - custom attributes (payGrade, hireDate, jobCode, etc.)

Custom schema extensions must be registered with the provisioning app before use. The provisioning service maps SCIM attributes to Entra ID or AD attributes through the standard attribute mapping configuration.

Matching Logic

The externalId field in the SCIM payload is the primary matching attribute. By default, it maps to employeeId in Entra ID. The provisioning engine uses this to determine whether a user already exists:

  • Match found: Update the existing user with the incoming data.
  • No match: Create a new user.
  • Record marked inactive ("active": false): Disable or soft-delete the matched user according to the deprovisioning action configured in the provisioning app.

Use Cases

HR System Integration

Any HR system that can export identity data (via API, database query, or file export) can serve as a source. The API client reads employee records from the HR system and sends them to the bulkUpload endpoint. This replaces legacy CSV import pipelines and eliminates the need for staging databases.

Custom Identity Feeds

ISV platforms, partner identity systems, or federated organization directories can push identity data directly to Entra ID. The API client handles authentication, data transformation to SCIM format, and the upload call.

Bulk Provisioning and Migration

For initial tenant population or migration from another identity provider, the API can batch-load user records. The 50-record-per-call limit with up to 2,000 (or 6,000) daily calls supports loading tens of thousands of users per day.

Student Information Systems

Educational institutions with custom SIS platforms can provision student and faculty accounts by pushing enrollment data through the API.

Configuration

Step 1: Create the Provisioning App

  1. In the Entra admin center, go to Enterprise applications > New application.
  2. Search the gallery for either:
    • API-driven provisioning to Microsoft Entra ID (for cloud-only users)
    • API-driven provisioning to on-premises Active Directory (for hybrid users)
  3. Create the application.

For on-premises AD targets, you also need to install the provisioning agent on a domain-joined Windows server. The agent initiates outbound connections to the cloud; no inbound firewall rules are required.

Step 2: Configure Attribute Mappings

  1. Go to the provisioning app’s Provisioning page > Edit provisioning.
  2. Under Mappings, select the user mapping.
  3. Review and customize the mappings from SCIM attributes to Entra ID (or AD) attributes.
  4. Key mappings to verify:
    • externalId to employeeId (matching attribute)
    • userName to userPrincipalName
    • active to account enabled status
    • Manager reference resolution (if using manager chain)

Step 3: Configure Scoping

Set scoping filters to control which incoming records are processed. By default, all records are in scope. You can add filters based on any SCIM attribute (e.g., only process records where department equals “Engineering”).

Step 4: Grant API Access

The API client needs an identity with the SynchronizationData-User.Upload permission. Three options:

  • App registration with client secret or certificate. Register an app in Entra ID, grant the permission, and use client_credentials OAuth flow.
  • Managed identity. If the API client runs in Azure (Logic App, Azure Function), assign a system-managed identity and grant the permission via PowerShell:
$servicePrincipal = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
$appRole = $servicePrincipal.AppRoles | Where-Object { $_.Value -eq "SynchronizationData-User.Upload" }
New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $managedIdentityId `
    -PrincipalId $managedIdentityId `
    -ResourceId $servicePrincipal.Id `
    -AppRoleId $appRole.Id
  • Delegated permissions. For interactive testing only (not recommended for production).

For reading provisioning logs, also grant AuditLog.Read.All or ProvisioningLog.Read.All.

Step 5: Start the Provisioning Service

  1. Set Provisioning Status to On.
  2. Save the configuration.

The service is now listening for bulkUpload requests. When data arrives, it processes records asynchronously using the configured mappings and scoping rules.

API Client Patterns

PowerShell with CSV2SCIM

Microsoft provides a CSV2SCIM PowerShell module for converting CSV files to SCIM bulk requests:

Install-Module CSV2SCIM
Import-Module CSV2SCIM

# Convert CSV to SCIM and upload
Import-Csv "employees.csv" | Send-ScimBulkRequest `
    -ServicePrincipalId $spId `
    -Body { $_ | ConvertTo-ScimBulkPayload }

The module handles batching (50 records per request), throttle-aware retry, and provisioning log retrieval.

cURL

# Get OAuth token
TOKEN=$(curl -s -X POST \
  "https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token" \
  -d "client_id={clientId}&client_secret={secret}&scope=https://graph.microsoft.com/.default&grant_type=client_credentials" \
  | jq -r '.access_token')

# Send bulk upload
curl -X POST \
  "https://graph.microsoft.com/beta/servicePrincipals/{spId}/synchronization/jobs/{jobId}/bulkUpload" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/scim+json" \
  -d @scim-payload.json

Azure Logic Apps

A Logic App workflow can:

  1. Trigger on a schedule or event (e.g., file drop in blob storage).
  2. Call an Azure Function to convert source data (CSV, JSON, SQL query results) to SCIM format.
  3. POST the SCIM payload to the bulkUpload endpoint using the Logic App’s managed identity for authentication.

This approach requires no custom code deployment and supports monitoring through Logic App run history.

Monitoring and Troubleshooting

Provisioning Logs

All inbound provisioning activity is recorded in the Entra provisioning logs:

  • Entra admin center: Monitor > Provisioning logs. Filter by the inbound provisioning app name.
  • Graph API:
GET https://graph.microsoft.com/beta/auditLogs/provisioning?$filter=jobid eq '{jobId}'

Each log entry shows the source record, matched target, applied mappings, and the action taken (create, update, skip, or error).

Common Errors

HTTP StatusCauseResolution
400 Bad RequestMalformed SCIM payload, missing required attributesValidate JSON structure and required SCIM fields
401 UnauthorizedInvalid or expired tokenRefresh the OAuth token; verify app registration
403 ForbiddenMissing SynchronizationData-User.Upload permissionGrant the permission to the API client identity
429 Too Many RequestsThrottle limit exceededImplement backoff; respect Retry-After header

Quarantine

If the provisioning service encounters repeated errors (e.g., connectivity issues, invalid mappings), it enters quarantine. In quarantine, the processing frequency drops and the service reports degraded status. Fix the root cause, then restart the provisioning cycle to clear quarantine.

UPN Conflicts

When provisioning to Entra ID, userPrincipalName must be unique across the tenant. If the API sends a record that maps to an existing UPN, provisioning fails for that record. Use a unique UPN generation expression in attribute mappings (e.g., append a number suffix) or pre-validate UPN uniqueness.

Manager Resolution

Manager references use externalId matching. The manager’s externalId in the SCIM payload must match an existing user’s employeeId in Entra ID. If the manager has not been provisioned yet, the manager reference fails. Solution: process managers before their reports, or run two passes (first pass creates users, second pass updates manager references).

Custom SCIM Schema Extensions

For attributes not covered by the standard SCIM schemas, register a custom extension:

  1. In the provisioning app’s attribute mapping page, select Show advanced options > Edit attribute list for API.
  2. Add the custom schema namespace (e.g., urn:ietf:params:scim:schemas:extension:contoso:1.0:User).
  3. Define attributes under the namespace (e.g., payGrade, costCenter, buildingCode).
  4. Map the custom attributes to Entra ID extension attributes or custom security attributes.

The API client must include the custom schema namespace in the schemas array and provide the attribute values in the corresponding namespace object within each SCIM user record.

Next Steps