# Set up SCIM provisioning

Source: https://startwithidentity.com/recipes/set-up-scim-provisioning/
Last updated: 2026-06-19
License: content by Start with Identity. Cite the source URL.

---

SCIM (System for Cross-domain Identity Management, RFC 7643 and RFC 7644) is the standard that lets an identity provider push user lifecycle changes into your application: create a user on hire, update attributes on a role change, and deactivate on offboarding. Implementing a SCIM server is what makes your app show up as provisionable in directories like Okta, Microsoft Entra ID, and others. For the protocol details, see [SCIM 2.0](https://startwithidentity.com/standards/scim-2-0/).

## The contract

A minimal but real SCIM 2.0 server for the `Users` resource implements:

```
GET    /scim/v2/Users           list and filter users
POST   /scim/v2/Users           create a user
GET    /scim/v2/Users/{id}      read one user
PUT    /scim/v2/Users/{id}      replace a user
PATCH  /scim/v2/Users/{id}      partial update (used heavily for activate/deactivate)
DELETE /scim/v2/Users/{id}      delete (some IdPs deactivate via PATCH instead)
```

Authenticate every request with a bearer token the IdP is configured with. Set `Content-Type: application/scim+json`.

## 1. The User resource shape

SCIM users follow a fixed schema. The fields connectors rely on most are `userName`, `active`, `emails`, and `name`.

```json
{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
  "id": "2819c223-7f76-453a-919d-413861904646",
  "userName": "jane@example.com",
  "name": { "givenName": "Jane", "familyName": "Doe" },
  "emails": [{ "value": "jane@example.com", "primary": true }],
  "active": true,
  "meta": {
    "resourceType": "User",
    "created": "2026-01-10T10:00:00Z",
    "lastModified": "2026-01-10T10:00:00Z",
    "location": "https://api.example.com/scim/v2/Users/2819c223-7f76-453a-919d-413861904646"
  }
}
```

## 2. Create

```ts
app.post('/scim/v2/Users', auth, async (req, res) => {
  const { userName, name, emails, active = true } = req.body;
  const existing = await db.findByUserName(userName);
  if (existing) {
    return res.status(409).json({
      schemas: ['urn:ietf:params:scim:api:messages:2.0:Error'],
      detail: 'User already exists',
      status: '409',
    });
  }
  const user = await db.create({ userName, name, emails, active });
  res.status(201).json(toScim(user));
});
```

## 3. PATCH: the operation that matters most

Identity providers deactivate users with a PATCH that sets `active` to `false` rather than deleting them. Handle the PatchOp format (RFC 7644 section 3.5.2).

```ts
app.patch('/scim/v2/Users/:id', auth, async (req, res) => {
  const ops = req.body.Operations ?? [];
  const user = await db.get(req.params.id);
  if (!user) return notFound(res);

  for (const op of ops) {
    if (op.op?.toLowerCase() === 'replace') {
      // op.path may be "active", or op.value may be an object of attrs
      if (op.path === 'active') user.active = op.value === true || op.value === 'True';
      else Object.assign(user, op.value);
    }
  }
  await db.save(user);
  res.status(200).json(toScim(user));
});
```

When `active` becomes `false`, terminate the user's sessions in your app. Deprovisioning that leaves live sessions open defeats the purpose.

## 4. List and filter

IdPs check for existing users with a filter on `userName` before creating. You must support at least the `eq` operator.

```ts
// GET /scim/v2/Users?filter=userName eq "jane@example.com"&startIndex=1&count=100
app.get('/scim/v2/Users', auth, async (req, res) => {
  const filter = String(req.query.filter ?? '');
  const m = filter.match(/userName eq "(.+?)"/i);
  const users = m ? await db.findByUserName(m[1]).then((u) => (u ? [u] : []))
                  : await db.list();
  res.json({
    schemas: ['urn:ietf:params:scim:api:messages:2.0:ListResponse'],
    totalResults: users.length,
    startIndex: Number(req.query.startIndex ?? 1),
    itemsPerPage: users.length,
    Resources: users.map(toScim),
  });
});
```

## 5. Errors in SCIM format

Every error must use the SCIM error schema, or connectors report opaque failures.

```json
{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
  "detail": "Resource 123 not found",
  "status": "404"
}
```

## Interoperability notes

- Test against the actual connector you target. Okta, Entra ID, and OneLogin each exercise slightly different subsets and PATCH shapes.
- Support both `DELETE` and PATCH-to-inactive: some IdPs deprovision one way, some the other.
- Return a stable `id` in `meta.location`. The IdP stores it and uses it for all later updates.
- Groups (`/Groups`) are a second resource type. Add it once Users is solid, if you need group-based provisioning.

Reference: [SCIM 2.0](https://startwithidentity.com/standards/scim-2-0/), RFC 7643 (schema) and RFC 7644 (protocol).
