### Change the plan of many subscribers

`POST /api/v1/users/bulk-change-profile`

The single-subscriber change-profile applied to each id: switch now (the current service ends and the subscriber re-activates on the new plan), or store the change for the subscriber's next expiry. The target plan is checked once, before any subscriber is touched.

- Authentication: manager session (JWT) or API token
- Permission: `prm_users_change_profile` (Change user profile)
- Risk: write
- Rate limit bucket: `t_mutate`

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `ids` | body | array | yes | Subscriber ids, at most 1000. |
| `new_profile_id` | body | integer | yes | A main (prepaid or postpaid), active plan the caller may assign. |
| `when` | body | string | no | immediate (default) or on_next_expiration. Any other value is a 400. |
| `reset_traffic` | body | boolean | no | Immediate changes only. Zeroes the remaining traffic and time. |

#### Request

```json
{
  "ids": [4711, 4712, 4713],
  "new_profile_id": 12,
  "when": "on_next_expiration"
}
```

#### Response — 200 OK

```json
{
  "data": {
    "updated": [4711, 4713],
    "scheduled": [4713],
    "failed": [
      { "id": 4712, "reason": "same_profile", "message": "Already on this plan." }
    ]
  }
}
```

#### Errors

| Code | Status | When |
| --- | --- | --- |
| `ERR_VALIDATION` | 400 | new_profile_id missing, the plan is not assignable or retired, or when is not a known value |

#### Note

scheduled lists the ids in updated whose change waits for expiry. A subscriber who is already expired is switched now even when on_next_expiration was asked for. Row reasons: not_authorized, not_found, same_profile, profile_not_assignable, profile_inactive, kyc_incomplete, manager_limit_profile_change_reached, error.

