Bulk Operations
The Bulk Operations page (also called Change Bulk in some menus) lets you apply one action to many subscribers in a single call. It is the right tool when you need to renew 5,000 customers at the start of the month, disconnect everyone on a plan you’re retiring, push a price change to a reseller’s whole book, or hand a sub-reseller’s subscribers off to a different reseller.
Every action runs server-side in a single transaction-per-row loop so that one bad subscriber doesn’t fail the batch. The result message tells you how many succeeded and how many were skipped. A single audit log row records the whole bulk action — not one row per subscriber.
How to get here
Section titled “How to get here”- Sidebar → Change Bulk (lightning-bolt icon).
- Direct URL:
/change-bulk.
Requires subscribers.change_bulk. Each specific action also requires its own permission — bulk renew needs subscribers.renew, bulk reset-FUP needs subscribers.reset_fup, etc. Admins bypass all checks.
Layout
Section titled “Layout”Two-column layout:
| Column | Purpose |
|---|---|
| Left (2/3) | Filter builder. Stack filters until you’ve narrowed to the subscribers you want. Sticky pagination at the bottom shows you the matching count. |
| Right (1/3) | Action panel. Pick the action, fill in the action value if required, click Apply. Confirmation modal with the count before the request fires. |
The right column is sticky — it stays in view as you scroll through matches on the left.
Filter builder
Section titled “Filter builder”Stackable filter pills. Each pill is an AND against the others. Available filters:
| Filter | Notes |
|---|---|
| Service plan | One or more plans. |
| Status | Active / inactive / expired / expiring / online / offline. |
| Reseller | One or more resellers; toggle to include sub-resellers. |
| FUP level | 0 (none), 1/2/3 (daily), 4/5/6 (monthly). |
| Expiry range | expiry_date BETWEEN x AND y. |
| Created range | created_at BETWEEN x AND y. |
| Last seen range | When the subscriber was last online. |
| Region / Country / City | Geographic. |
| Payment method | cash, card, etc. |
| Search | Free text across username, name, phone, email, address. |
The match-count badge updates as you add filters. When it shows the intended count, the action becomes “safe to fire”.
The action list
Section titled “The action list”Selected action determines the right-hand panel. Each action calls the BulkAction handler with { ids, action, action_value }. The full list:
| Action | What it does | Permission | Action value |
|---|---|---|---|
renew | Extend expiry_date by N days (default 30), reset daily FUP, charge reseller balance per subscriber. | subscribers.renew | days |
disconnect | CoA disconnect via MikroTik API pool (audit perf fix v1.0.546). | subscribers.disconnect | — |
enable / disable (set_active/set_inactive) | Toggle subscriber status. | subscribers.inactivate | — |
reset_fup | Zero daily/monthly FUP counters, restore service-default rate in radreply. | subscribers.reset_fup | — |
reset_monthly_fup | Zero only monthly counters. | subscribers.reset_fup | — |
reset_all_counters | Daily + monthly + CDN counters. | subscribers.reset_fup | — |
delete | Soft delete; remove radcheck/radreply. | subscribers.delete | — |
set_expiry | Set absolute expiry date. v1.0.551 bounds: within −1 / +10 years of now. | subscribers.edit | date |
add_days | Add N days to current expiry. | subscribers.renew | days |
set_service | Switch to a new service plan and update price. | subscribers.change_service | service ID |
set_reseller | Move to a different reseller. v1.0.546–547 whitelist: a non-admin caller can only move subscribers to their own reseller or a direct child sub-reseller; security audit refused any other target. | subscribers.edit + scope check | reseller ID |
set_static_ip | Set static_ip and write radreply Framed-IP-Address. v1.0.551 IPv4 validation — invalid strings (Unicode, partial CIDR, IPv6) refused before write. Empty string clears. Duplicate detection prevents two users with the same IP. | subscribers.edit | IP |
set_monthly_quota | Set monthly_quota in bytes. v1.0.551 bounds: 0–100 TB (102,400 GB). | subscribers.edit | GB |
set_daily_quota | Set daily_quota in bytes. v1.0.551 bounds: 0–10 TB (10,485,760 MB). | subscribers.edit | MB |
set_price | Set per-subscriber price override. v1.0.551 bounds: 0–1,000,000. | subscribers.edit | amount |
set_password | Hash with bcrypt, update radcheck Cleartext-Password. | subscribers.edit | password |
reset_mac | Clear MAC and radcheck Calling-Station-Id. | subscribers.reset_mac | — |
set_nas | Assign to a different NAS. | subscribers.edit | NAS ID |
refill | Add quota from prepaid card. | subscribers.refill_quota | code |
rename | Change username across subscribers, radcheck, radreply, radacct. | subscribers.rename | new username |
The v1.0.551 sanity bounds on quota, price, expiry date, and static IP came out of the 2026-05-12 security audit; each refusal is logged with SECURITY: BulkAction <action> refused — ... so you can detect a fat-finger or a hostile reseller.
How execution works
Section titled “How execution works”POST /api/subscribers/bulk-action body:
{ "ids": [123, 456, 789, ...], "action": "renew", "action_value": "30"}The handler:
- Permission gate: refuses unauthorised callers based on the action.
- Scope: a reseller without
subscribers.view_allis restricted to their own subscribers (and sub-resellers’ subscribers) before the loop begins — even if a malicious client sends IDs outside that scope. - Pre-loads the caller’s reseller once (avoids N+1 balance lookups during
renew). - Pre-loads every NAS used by the selected subscribers (avoids N+1 queries for disconnect / CoA paths).
- Loops the selected IDs; for each one, applies the action and increments
successorfailedcounters. - Records a single audit log row:
ChangeBulk: <ActionName> for <success> subscribers (<failed> failed). - Returns
{ success, failed, total }.
The disconnect path uses the MikroTik connection pool rather than mikrotik.NewClient — at ~10 ms per fresh TCP+login handshake, a 1,000-subscriber disconnect without pooling adds ~10 seconds of wall-clock latency (audit 2026-05-11 backend perf HIGH). Sibling paths at lines ~2488 and ~2523 already use the pool; the bulk path was patched to match.
Why some rows fail
Section titled “Why some rows fail”- The subscriber was deleted between filter time and action time.
- For
renew: the caller is a reseller and their balance is below the subscriber’s price. - For
set_static_ip: another user already holds that IP in radreply. - For
set_service: the service ID doesn’t exist. - For
set_reseller: the target reseller isn’t allowed by the v1.0.547 whitelist. - For
set_*_quota/set_price/set_expiry: the value violated the v1.0.551 sanity bounds.
Failure counts are reported in the response. The audit row’s description carries both counts so you can drill into “which 3 failed?” later by filtering the audit log.
Common workflows
Section titled “Common workflows”Renew everyone whose plan is “10M Family” at the start of the month
Section titled “Renew everyone whose plan is “10M Family” at the start of the month”- Filter: service =
10M Family, status =active. - Confirm the count matches your billing system’s expectation.
- Action =
Renew, days = 30. - Confirm the modal. Wait for the result toast.
- If failures > 0, open Audit Log, find the bulk row, drill into the subscribers’ detail pages to see why each failed.
Move 200 subscribers from one sub-reseller to another
Section titled “Move 200 subscribers from one sub-reseller to another”- Filter: reseller = the source sub-reseller.
- Action =
Set reseller, target = the destination sub-reseller. (Permission gate: the destination must be your own reseller or a direct child of yours; admins bypass.) - Confirm. The 200 subscribers’
reseller_idflips in a single batch. - The audit log shows you (the actor), the source, the destination, and the count.
Apply a price increase to a reseller’s whole book
Section titled “Apply a price increase to a reseller’s whole book”- Filter: reseller = the affected reseller.
- Action =
Set price, value = the new amount. - Confirm. Every selected subscriber’s
priceis updated; the next renewal will charge the new amount. - Optional: trigger Communication Rules to notify each subscriber of the change.
Permissions
Section titled “Permissions”| Permission | What it gates |
|---|---|
subscribers.change_bulk | Open the page and submit bulk requests. |
| Per-action permissions | Each action also requires its own permission as shown in the action table. |
subscribers.view_all | Without it, a reseller is restricted to their own subscribers and sub-resellers’ subscribers. |
Related pages
Section titled “Related pages”- Subscribers — the list view where you select rows and trigger the same actions per-row.
- Audit Logs — every bulk action writes one audit row with success/failure counts.
- Users & Permissions — configure which resellers can do which bulk action.
- Reports — confirm the impact of a bulk action after the fact.
- Billing & Invoices — bulk renew generates invoices and transactions.