
# Simulate SCIM provisioning

`POST /v1/directories/{directory_id}:simulate` · permission `directories:write` · development and staging only

Runs one SCIM simulator scenario against the directory's real SCIM endpoint. For the run only, KeenAuth mints a real
SCIM token, uses it, and then revokes it. Token checks, rate limits, the mass deprovision circuit breaker and the
provisioning log all run exactly as they do for your identity provider. The users are a fixed fictional set: 25 people
at `example.test` in 5 groups. They contain no real personal data.

The simulator is a development tool and changes the directory. The users and groups it writes stay in the directory
like any other provisioned records. Use a directory you created for testing.

| Scenario | What it does |
|---|---|
| `seed` | Creates the 25 users and the 5 groups with their memberships. In a directory that already holds them, the first user gets a SCIM `409` and the run is `failed`. |
| `bulk_deactivate` | Sends `active=false` for 60% of the users (15 of 25). The circuit breaker trips only when the directory's `mass_deprovision_guard.absolute_min` is 15 or lower. With the default of 25 it does not trip. |
| `group_churn` | Moves a user from one group to another. |
| `attribute_update` | Changes every user's display name and family name. |
| `conflict_email` | Creates a user with an existing `userName` and expects a SCIM `409 uniqueness`. |

When the directory is empty, every scenario except `seed` runs `seed` first.

The simulation token is never listed and does not count toward the 2 valid tokens. Using it does not start the
rotation of your identity provider's token. Only one run per directory can be in progress at a time. The run is audited
as `directory.simulation_started` and `directory.simulation_finished`.

## Request

| Field | Type | Required | Description |
|---|---|---|---|
| `directory_id` | path | yes | SCIM directory ID (`dir_…`). |
| `scenario` | body, string | yes | `seed`, `bulk_deactivate`, `group_churn`, `attribute_update` or `conflict_email`. |
| `Idempotency-Key` | header | no | Replays the first response for a retried request; the scenario does not run again. |

```json
{"scenario": "bulk_deactivate"}
```

## Response

`200 OK` with the run's result. A SCIM error answer does not fail the request. It makes the run `failed`, fills
`failure` and leaves the counts at what succeeded before the error. The result has counts and states only: no user, no
SCIM ID and no token.

```json
{
  "directory_id": "dir_01j9xm2n4p6q8r0s2t4v6w8y0z",
  "scenario": "bulk_deactivate",
  "status": "completed",
  "users_created": 0,
  "groups_created": 0,
  "users_updated": 0,
  "groups_updated": 0,
  "users_deactivated": 15,
  "conflicts": 0,
  "circuit_breaker_tripped": true,
  "deprovision_hold": {"id": "0191e9b2-6f3a-7c4d-8e5f-0a1b2c3d4e5f", "pending_count": 6},
  "failure": null,
  "started_at": "2026-10-28T09:31:00.123456Z",
  "completed_at": "2026-10-28T09:31:01.654321Z"
}
```

| Field | Description |
|---|---|
| `status` | `completed`, or `failed` when the endpoint answered with a SCIM error, the run took too long, or an answer was not what the scenario expected. |
| `users_updated` | User PATCHes other than deactivations. |
| `users_deactivated` | `active=false` PATCHes the endpoint accepted, held ones included. |
| `conflicts` | Expected `409 uniqueness` answers (`conflict_email`). |
| `circuit_breaker_tripped` | A mass deprovision hold was opened during the run. |
| `deprovision_hold` | The directory's open hold after the run, or `null`. Approve or reject it with the `deprovision-holds` endpoints. |
| `failure` | `null` when `completed`. Otherwise `reason` (`scim_error`, `timeout` or `unexpected_response`), and for `scim_error` also `scim_status` (HTTP status) and `scim_type` (SCIM `scimType`, or `null`). |

## Errors

| HTTP | code | When |
|---|---|---|
| 400 | `unknown_field` | The body contains a field other than `scenario`. |
| 403 | `test_feature_not_in_production` | The environment is a production environment. |
| 404 | `not_found` | The directory does not exist in this environment. |
| 409 | `conflict` | Another run of this directory is in progress, or the directory is deactivated or being deleted. |
| 422 | `validation_failed` | `scenario` is missing or unknown (`/scenario`). |
| 422 | `idempotency_key_reuse` | The `Idempotency-Key` was used for a different request. |
| 401 | `unauthenticated` | The credential is missing, invalid or revoked. |
| 403 | `insufficient_permissions` | The credential lacks the required permission. |
| 429 | `rate_limited` | The rate limit was exceeded; retry after the `Retry-After` period. |
| 503 | `service_unavailable` | Directory sync or the simulator is temporarily unavailable or not configured in this deployment; retry after the `Retry-After` period. |

## Example

```bash
curl -X POST "https://api.eu.keenauth.com/v1/directories/dir_01j9xm2n4p6q8r0s2t4v6w8y0z:simulate" \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"scenario": "seed"}'
```

```bash
keenauth directories simulate dir_01j9xm2n4p6q8r0s2t4v6w8y0z --scenario bulk_deactivate
```
