
# Create a bulk user deletion job

`POST /v1/jobs/user-deletions` · permissions `users:delete` and `jobs:write`

Deletes up to 10,000 users in one asynchronous job. Each user goes through the same deletion as [`DELETE /v1/users/{user_id}`](/docs/api/management/users/delete): the user becomes `pending_deletion` and the permanent deletion runs afterwards; the deletion record notes that it came from a bulk job. Deletion cannot be undone, so the endpoint works in two steps:

1. Send the request with `"dry_run": true`. Nothing is written; the response holds `matched_count`, the number of users the request would delete.
2. Send the same selection with `"confirm_count"` set to that number. If the number of matching users has changed in between, the request fails with `409 confirmation_mismatch` and no job is created; run the dry run again.

Select users in one of two ways: `user_ids` (repeated ids count once; results keep the order of first appearance) or `organization_id` (the organization's active and suspended members at the moment the job is created). Ids that do not match a user are kept in the job and reported as `not_found`.

Only one bulk user deletion job runs at a time in an environment; while one is running, a new job is refused with `429 rate_limited` and `Retry-After`. The job is written to the management audit log when it is created and when it completes; each deleted user also has its own deletion audit entry. Follow the job with [Get a bulk user deletion job](/docs/api/management/jobs/get-user-deletion) and read per-user outcomes with [List bulk user deletion results](/docs/api/management/jobs/list-user-deletion-results).

## Request

**Headers**: `Idempotency-Key` (required, 16–64 characters `A-Z a-z 0-9 _ -`). Repeating the request with the same key and the same body returns the first job; the same key with a different body fails with `422 idempotency_key_reuse`. The body may be up to 512 KiB.

| Field | Type | Required | Description |
|---|---|---|---|
| `user_ids` | array of strings | one of the two | User ids prefixed `usr_`, 1 to 10,000. |
| `organization_id` | string | one of the two | Organization id prefixed `org_`; at most 10,000 members. |
| `dry_run` | boolean | no | `true`: only count the users; nothing is written. Default `false`. |
| `confirm_count` | integer | yes, unless `dry_run` | The `matched_count` of the dry run (0–10,000). |

## Response

Dry run: `200 OK` with `{"dry_run": true, "matched_count": n}`.

Job created: `202 Accepted` with the count and the job.

```json
{
  "dry_run": false,
  "matched_count": 2,
  "job": {
    "id": "job_01h455vb4pex5vsknk084sn02q",
    "status": "running",
    "filter": "user_ids",
    "total": 3,
    "succeeded": 0,
    "not_found": 0,
    "failed": 0,
    "pending": 3,
    "created_at": "2026-01-15T09:30:00Z"
  }
}
```

## Errors

Every error is an `application/problem+json` body with a stable `code`.

| HTTP | `code` | When |
|---|---|---|
| 400 | `malformed_request`, `unknown_field` | The `Idempotency-Key` header is missing or invalid, or the body is not valid JSON or has an unknown field. |
| 401 | `unauthenticated` | The API key is missing, invalid or revoked. |
| 403 | `insufficient_permissions` | The key lacks `users:delete` or `jobs:write`. |
| 409 | `confirmation_mismatch` | `confirm_count` is not the current number of matching users. |
| 409 | `idempotency_in_progress` | A request with the same `Idempotency-Key` is still being processed. |
| 413 | `payload_too_large` | The body is larger than 512 KiB. |
| 422 | `validation_failed` | Both or neither of `user_ids` and `organization_id`, a malformed id, more than 10,000 users (`too_many`, `too_many_users`), an organization without members (`no_users`) or a missing `confirm_count` (`required`). |
| 422 | `idempotency_key_reuse` | The `Idempotency-Key` was used with a different body. |
| 429 | `rate_limited` | Another bulk user deletion job is running, or the rate limit was exceeded; honor `Retry-After`. |
| 503 | `service_unavailable` | A dependency is temporarily unavailable; retry later. |

## Example

```bash
curl -X POST https://api.eu.keenauth.com/v1/jobs/user-deletions \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"organization_id": "org_01h455vb4pex5vsknk084sn02q", "dry_run": true}'
```

CLI: `keenauth jobs user-deletions create --organization-id org_01h455vb4pex5vsknk084sn02q --dry-run --yes`, then `keenauth jobs user-deletions create --organization-id org_01h455vb4pex5vsknk084sn02q --confirm-count 42 --yes`. Long id lists go in a file: `--data @users.json`.
