Skip to content

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_MANAGER
  • SESSION_ADMIN
  • SESSION_DEALER
  • SESSION_CRM_MANAGER
  • SESSION_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_accounts and confirm are 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 failures and 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.
  • limit and offset are required by the schema but have no effect. Every customer matching the filter is processed.
  • orderBy is 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

  1. Customer brand and desk scope. A failure yields PERMISSION_DENIED.
  2. One pass over that customer's accounts: scope, blocking positions, and balance are all evaluated before anything is written.
  3. Any account out of scope, or any blocking position, cancels the whole customer. Nothing is touched.
  4. With confirm: false, processing stops here and only the counters are returned.
  5. Accounts are deleted, or detached, one at a time. The first failure stops this customer immediately.
  6. 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

  1. Run with confirm: false and read matched, accounts_matched, and accounts_with_balance.
  2. If accounts_with_balance is higher than expected, stop. The filter is probably wrong.
  3. Collect the logins from every ACCOUNT_HAS_TRADES entry and close those positions.
  4. Re-run the dry run until failed is zero, or until the remaining failures are understood and accepted.
  5. Run with confirm: true.
  6. On 207, read failures: entries carrying accounts_deleted mark 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.