MngDeleteCustomersByFilter¶
Deletes every customer matching the supplied filter. A single flag decides what
happens to the trading accounts linked to those customers:
delete_accounts: false detaches them, delete_accounts: true deletes them.
The confirm flag decides whether anything is written at all. With
confirm: false the command runs every check and returns the counters
describing what would happen, without touching a single record. With
confirm: true the same run also performs the deletion. The response shape is
identical in both modes.
This is the destructive member of the bulk customer family, alongside MngUpdateCustomersByFilter and its three narrow siblings. It shares their filter, their response shape, and their status codes. To delete a single customer record without touching accounts, use MngDeleteCustomer.
Nothing here can be undone
Deleted customers, accounts, and trades are removed from storage, not archived. Recovery is possible only from a backup. Treat the dry run as a mandatory step, not a convenience.
SEO¶
Page summary: Bulk-delete CRM customers through the ScaleTrade manager TCP API, optionally deleting or detaching their trading accounts, with a dry-run preview.
Keywords: ScaleTrade customer API, MngDeleteCustomersByFilter, bulk delete customers, mass delete CRM customers, delete customer with accounts, unlink trading account, dry run bulk delete, manager TCP API.
Access Control¶
Allowed sessions:
SESSION_MANAGERSESSION_ADMINSESSION_DEALERSESSION_CRM_MANAGERSESSION_CRM_ADMIN
Session type is checked as explicit set membership, not as a privilege ladder.
Authority is layered: some of it gates the command as a whole, the rest is
decided by the delete_accounts flag and re-checked against every individual
record.
| To do this | The caller needs |
|---|---|
| Call the command at all | access_crm plus del_customers plus set_customers |
delete_accounts: false |
the above, plus the brand of every account's group must match the caller's brand |
delete_accounts: true |
the above, plus access_backoffice plus del_accounts, plus every account's group must fall inside the caller's groups mask |
Why set_customers is required even for deletion. Permissions in this
system are independent flags, not a ladder: del_customers does not imply
set_customers. The detach branch performs exactly what
MngUnlinkAccountFromCustomer
performs, and that command is governed by set_customers, so this one requires
it too.
Why the two branches differ on accounts. Deleting an account is a
back-office action and is checked by the same rule as the standalone
MngDeleteAccount: brand plus the groups mask,
both gated behind access_backoffice. Detaching is a CRM action, since it only
clears the customer_id field, so it is bounded by the tenant brand alone. A
CRM administrator with no back-office access can therefore delete customers with
the detach branch, but not with the delete branch.
A caller who asks for delete_accounts: true without del_accounts is refused
with 403 up front, before any customer is selected. A super administrator
bypasses all of this at the router, before the handler runs.
Two masks operate independently: desks bounds which customers are visible,
groups bounds which accounts may be touched. An empty mask means everything
within the brand, not nothing.
Behavior¶
delete_accountsandconfirmare both mandatory. There are no defaults, so an incomplete request can never destroy anything.- Each customer is all-or-nothing. If any check fails, that customer is reported
in
failuresand none of their accounts are touched. - A failed customer means zero changes for that customer, not a partial deletion. The single exception is the race described below, and it is always visible in the response.
- Accounts are processed first, the customer last. If the run breaks mid-way, the customer survives and the surviving accounts stay attached to a living record. The state stays consistent, and re-running the same request finishes the job.
- An open market position blocks deletion. Any account holding a BUY or SELL in
an active state fails the customer with
ACCOUNT_HAS_TRADES, and the offending logins are listed. - Pending orders do not block and disappear silently. Only market positions count as blocking. A customer with fifty pending orders deletes without a word, and the pendings go with the account. This matches the standalone MngDeleteAccount.
- A non-zero balance does not block either. It is reported, not enforced; see
accounts_with_balance. - Blocking checks do not apply to the detach branch: detaching an account with an open position is harmless.
limitandoffsetare required by the schema but have no effect. Every customer matching the filter is processed.orderByis accepted for schema compatibility but has no observable effect.- Failure is partial across customers: one failing customer does not stop the run.
- Every call is written to the audit log with the caller id, mode, flag, and all counters.
Order Of Operations, Per Customer¶
- Customer brand and desk scope. A failure yields
PERMISSION_DENIED. - One pass over that customer's accounts: scope, blocking positions, and balance are all evaluated before anything is written.
- Any account out of scope, or any blocking position, cancels the whole customer. Nothing is touched.
- With
confirm: false, processing stops here and only the counters are returned. - Accounts are deleted, or detached, one at a time. The first failure stops this customer immediately.
- Only if every account succeeded, the customer record is deleted.
The One Race, And How To Read It¶
Checks run immediately before the writes for each customer, so the gap is
milliseconds rather than the length of the request, but it is not zero, because
a pending order becomes a market position from price movement, with no client
action. If that happens mid-customer, the account deletion is refused, the
customer is kept, and the failure entry carries accounts_deleted showing how
many accounts were already destroyed. This is the only case where a failed
customer is not a clean no-op, and the response always says so.
The Dry Run Predicts Exactly¶
The checks performed with confirm: false are the same ones that run when
applying, so the preview is a prediction rather than an estimate, accurate up to
that same race.
What Gets Destroyed¶
The flag's name understates its reach. Deleting an account also removes that account's entire trade history from storage, and that history includes the balance operations.
| Record | delete_accounts: false |
delete_accounts: true |
|---|---|---|
| Customer | Deleted | Deleted |
| Trading accounts | Kept, customer_id cleared |
Deleted |
| Closed trades | Kept | Deleted |
| Pending orders | Kept | Deleted |
| Deposits and withdrawals | Kept | Deleted |
| Open market positions | Kept | Block the deletion |
Funded accounts vanish without a trace
Nothing prevents deleting an account that still holds money, and the balance
operations that put it there are deleted along with it. From the reports'
point of view the funds simply cease to exist, with no closing entry. This
is why the dry run reports accounts_with_balance: a non-zero value on a
run you expected to be empty means stop and re-check the filter.
Request¶
Dry run, delete customers together with their accounts:
{
"command": "MngDeleteCustomersByFilter",
"extID": "1",
"data": {
"delete_accounts": true,
"confirm": false,
"filter": {
"deskFilter": "*",
"limit": 100,
"offset": 0,
"where": [
["lifecycle_stage", "=", 6],
["brand", "=", "ALFA"]
]
}
}
}
Apply, delete customers and keep the accounts detached:
{
"command": "MngDeleteCustomersByFilter",
"extID": "2",
"data": {
"delete_accounts": false,
"confirm": true,
"filter": {
"deskFilter": "DESK_EU*",
"limit": 100,
"offset": 0,
"where": [
["status", "=", 2]
]
}
}
}
Request Data¶
Top level:
| Field | Type | Required | Description |
|---|---|---|---|
delete_accounts |
bool | Yes | true deletes the linked accounts and their trade history. false keeps them and clears customer_id |
confirm |
bool | Yes | false reports what would happen without writing. true performs the deletion |
filter |
object | Yes | Customer selection, same parameter set as MngGetCustomersByFilter |
The filter object is described in Filter.
Filter¶
| Field | Type | Required | Description |
|---|---|---|---|
deskFilter |
string | Yes | Desk wildcard mask, for example *, DESK_*, !DESK_EU_TEST. Narrowed to the desks visible to the caller |
limit |
int | Yes | From 1 to 50000. Required by the schema, does not limit the deletion |
offset |
int | Yes | Minimum 0. Required by the schema, does not limit the deletion |
where |
array | No | [[field, operator, value], ...], all conditions must match |
orWhere |
array | No | OR comparison group; at least one condition must match |
whereNot |
array | No | [[field, value], ...] |
whereIn |
array | No | [[field, [values...]], ...] |
whereNotIn |
array | No | [[field, [values...]], ...] |
whereBetween |
array | No | [[field, [from, to]], ...] |
whereNotBetween |
array | No | [[field, [from, to]], ...] |
orderBy |
array | No | Accepted for compatibility, no observable effect |
Supported filter fields are the customer identity, contact, CRM, acquisition,
compliance, and lifecycle fields, the same set as
MngGetCustomersByFilter. String comparisons
support =, !=, and like, where like is a case-insensitive substring
match. Common filter semantics, including grouped orWhere, are described in
Table filter syntax; desk masks are described in
Desks TCP API.
A Filter With No Conditions Selects Everything¶
deskFilter: "*" with no where matches every customer the caller can see. The
command has no built-in guard against that. The dry run is the guard.
Response Data¶
Counters only, with no per-customer rows, so the payload is the same size for four customers or forty thousand.
Dry run:
{
"confirm": false,
"delete_accounts": true,
"matched": 40,
"deleted": 0,
"failed": 3,
"accounts_matched": 180,
"accounts_deleted": 0,
"accounts_unlinked": 0,
"accounts_with_balance": 47,
"failures": [
{"customer_id": 1099, "error": "PERMISSION_DENIED"},
{
"customer_id": 1104,
"error": "ACCOUNT_HAS_TRADES",
"message": "Accounts have open market positions",
"logins": [500123, 500124]
},
{
"customer_id": 1150,
"error": "ACCOUNT_HAS_TRADES",
"message": "Accounts have open market positions",
"logins": [500311]
}
]
}
Read as: 40 customers matched, 3 will not pass, the remaining 37 will be deleted along with 180 accounts, 47 of which still hold money.
Applied, partial, with the race made visible:
{
"confirm": true,
"delete_accounts": true,
"matched": 10,
"deleted": 9,
"failed": 1,
"accounts_matched": 44,
"accounts_deleted": 41,
"accounts_unlinked": 0,
"accounts_with_balance": 12,
"failures": [
{
"customer_id": 1207,
"error": "ACCOUNT_DELETE_FAILED",
"message": "Account has open trades",
"logins": [500780],
"accounts_deleted": 2
}
]
}
| Field | Type | Description |
|---|---|---|
confirm |
bool | Echo of the request flag |
delete_accounts |
bool | Echo of the request flag |
matched |
int | Customers selected by the filter, brand scope, and desk mask |
deleted |
int | Customers actually deleted. Always 0 when confirm is false |
failed |
int | Customers that did not pass. One entry in failures each |
accounts_matched |
int | Accounts belonging to customers that passed every check, the blast radius. Accounts of failed customers are not counted, because they are not touched |
accounts_deleted |
int | Accounts actually deleted. Always 0 in a dry run and in the detach branch |
accounts_unlinked |
int | Accounts actually detached. Always 0 in a dry run and in the delete branch |
accounts_with_balance |
int | How many of accounts_matched hold a non-zero balance. A count, never a sum: accounts sit in different currencies and no conversion is performed. Credit and bonus are not counted |
failures |
array | Present only when non-empty. Each entry carries customer_id, error, an optional message, an optional logins array naming the accounts at fault, and, only when a run broke mid-customer, accounts_deleted or accounts_unlinked showing what was already done |
Reading The Counters¶
After an applied run the identity matched = deleted + failed holds. On a dry
run deleted is 0 by definition, and the number of customers that would be
deleted is matched - failed.
Response Statuses¶
| Status | Condition |
|---|---|
200 |
Dry run. Always, since nothing was meant to change |
200 |
Applied, at least one customer deleted, no failures |
207 |
Applied, at least one customer deleted, some failed |
400 |
Schema validation failed |
403 |
delete_accounts: true without del_accounts and back-office access, or the caller context could not be resolved |
409 |
Applied, no customer deleted |
409 Has Two Meanings, Separate Them By The Counters¶
matched: 0 means the filter selected nobody, which is usually a mistake in the
filter. A non-zero failed equal to matched means every selected customer was
blocked, and failures says why.
Errors¶
Request-level:
| Error | Status | Description |
|---|---|---|
INVALID_DATA |
400 |
Schema validation failed: a missing or wrongly typed delete_accounts, confirm, or filter |
RET_NOT_ENOUGH_RIGHTS |
403 |
delete_accounts: true requested without del_accounts or without back-office access. Raised before any customer is selected |
RET_NOT_ENOUGH_RIGHTS |
403 |
Caller context could not be resolved: not found, disabled, or missing the required scope or permissions |
Per-customer, reported inside failures:
| Error | Carries logins |
Description |
|---|---|---|
PERMISSION_DENIED |
No | The customer is outside the caller's brand or desk scope |
PERMISSION_DENIED |
Yes | One or more accounts are outside the caller's scope. The message names the reason: the groups mask in the delete branch, a different brand in the detach branch |
ACCOUNT_HAS_TRADES |
Yes | One or more accounts hold open market positions. Close them and re-run |
ACCOUNT_DELETE_FAILED |
Yes | An account deletion failed after the pre-check passed. Carries accounts_deleted for this customer |
ACCOUNT_UNLINK_FAILED |
Yes | An account could not be detached. Carries accounts_unlinked for this customer |
RET_NOT_FOUND |
No | The customer disappeared between selection and deletion |
RET_ERROR |
No | Storage or internal error while deleting the customer record |
The Operation Is Not A Transaction¶
Customers, accounts, and trades live in three independent managers with three independent write queues. There is no cross-manager rollback. What the command guarantees instead is ordering: accounts are always processed before the customer, so an interrupted run leaves a consistent, retryable state rather than orphaned records.
Operator Workflow¶
- Run with
confirm: falseand readmatched,accounts_matched, andaccounts_with_balance. - If
accounts_with_balanceis higher than expected, stop. The filter is probably wrong. - Collect the
loginsfrom everyACCOUNT_HAS_TRADESentry and close those positions. - Re-run the dry run until
failedis zero, or until the remaining failures are understood and accepted. - Run with
confirm: true. - On
207, readfailures: entries carryingaccounts_deletedmark customers whose state moved and need a second look. Re-running the same request finishes the rest.
See also MngDeleteCustomer for a single record, MngUpdateCustomersByFilter for the non-destructive general form of the same contract, MngGetCustomerAccounts for listing the accounts of one customer before a run, MngDeleteAccount and MngUnlinkAccountFromCustomer for the two account branches on their own, and MngGetCustomersByFilter for the selection, filtering, and scope rules.