delete_orgs_bulk

Changes data · you confirm first Idempotent

Calls DELETE /api/orgs/bulk as the signed-in person · REST reference

Soft-delete several organizations as one all-or-nothing deletion, or preview it

The organizations are planned, guarded and deleted as ONE set in ONE DAL transaction — all of them or none. Each cascades exactly as DELETE /api/orgs/{id} does. The guard judges the SET: it refuses (409) any deletion that would leave no live holder of the PFF_SUPERADMIN role, including one where the orgs are each safe to delete alone but hold every superadmin between them. ?dryRun=true answers the same shape with no write — and a refused plan is then a 200 with blocked: true and the reason, which is how the portal shows it before confirming. Requires execute:DeleteOrgCommand. The caller's own token is forwarded. Registered BEFORE DELETE /api/orgs/{id}, so the literal bulk segment is not captured by the param route.

Arguments

ArgumentTypeDescription
dryRunquerystringThe string "true" previews the deletion without performing it. Compared as a string, so ?dryRun=1 deletes.
bodyJSONobjectrequiredThe request body, as a JSON object. Its fields are listed below.

Fields of body

FieldTypeDescription
orgIdsarray of string (uuid)required

Example call

A client sends this over the MCP session; Claude and other agents build it for you. The values are placeholders — the shape is real.

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "delete_orgs_bulk",
    "arguments": {
      "body": {
        "orgIds": [
          "11111111-1111-4111-8111-111111111111"
        ]
      }
    }
  }
}
Confirmation
The client shows the person the exact request and waits. Nothing runs until they tick Make this change. See Reading and changing.

Result

On success (HTTP 200) the tool returns one text item holding the API's JSON, pretty-printed.

What was deleted — or, with dryRun, what would be.

json
{
  "blocked": true,
  "blockedReason": "string",
  "dryRun": true,
  "message": "string",
  "reports": [
    {
      "blocked": true,
      "blockedReason": "string",
      "blockers": [],
      "groupsRemoved": [],
      "membershipsRemoved": 1,
      "orgId": "string",
      "orgName": "string",
      "permissionsRetired": 1,
      "rolesRemoved": [],
      "usersDeactivated": [],
      "usersKept": []
    }
  ]
}

Fields

FieldTypeDescription
blockedbooleanAlways false when dryRun is false — a refused deletion answers 409.
blockedReasonstring | null
dryRunboolean
messagestring
reportsarray of object
reports[].blockedboolean
reports[].blockedReasonstring | null
reports[].blockersarray of objectEvery reason this organization may not be deleted yet; empty when it may.
reports[].blockers[].amountCentsinteger | nullThe money involved, in cents, where there is one.
reports[].blockers[].code"LAST_SUPERADMIN" | "OPEN_CLAWBACK" | "PAYOUTS_PAUSED_BY_CLAWBACK" | "CLAWBACK_WINDOW_OPEN" | "BALANCE_OWED" | "PAYOUT_TRANSFER_UNSETTLED" | … (13 values)
reports[].blockers[].countintegerHow many of the thing the code names.
reports[].blockers[].messagestringThe sentence a person reads — what holds the deletion and what clears it.
reports[].blockers[].untilinteger | nullEpoch milliseconds the blocker lifts by itself, where it does (the clawback window).
reports[].groupsRemovedarray of objectThe organization's live groups — its OWNER and MEMBER groups among them; each is soft-deleted.
reports[].groupsRemoved[].idstring
reports[].groupsRemoved[].namestring | null
reports[].membershipsRemovedintegerCurrent (non-REMOVED) memberships into those groups; each is stamped REMOVED.
reports[].orgIdstring
reports[].orgNamestring
reports[].permissionsRetiredintegerThe organization's per-org permissions. Not written: every grant path requires a live organization, so they confer nothing once it is deleted.
reports[].rolesRemovedarray of objectThe organization's own live roles (its OWNER and ORG_USER roles); each is soft-deleted.
reports[].rolesRemoved[].idstring
reports[].rolesRemoved[].namestring | null
reports[].usersDeactivatedarray of objectMembers who belong to no other live organization. Each is soft-deleted and their DynamoDB auth row suspended, so they cannot sign in by either path.
reports[].usersDeactivated[].emailstring | null
reports[].usersDeactivated[].idstring
reports[].usersKeptarray of objectMembers of another live organization. Kept, and lose only this organization's memberships; keptBy names the organization(s) that keep them.
reports[].usersKept[].emailstring | null
reports[].usersKept[].idstring
reports[].usersKept[].keptByarray of object

When it fails

A failed call returns isError: true with the API's message as text. A change the person declines, or lets time out, also comes back this way and nothing changes.

400

orgIds absent, empty, not an array, holding a non-string — or holding an id that is not a UUID.

403

Authenticated, but the caller holds none of the required permissions.

404

An organization in the set does not exist or is already deleted; nothing was deleted.

409

The guard refused — the deletion would leave no live PFF_SUPERADMIN holder — or the graph changed between the plan and the write. Nothing was deleted.

What the model is shown

This is the tool exactly as the server lists it in tools/list. The description starts with the REST call it makes.

json
{
  "name": "delete_orgs_bulk",
  "description": "DELETE /api/orgs/bulk — The organizations are planned, guarded and deleted as ONE set in ONE DAL transaction — all of them or none. Each cascades exactly as `DELETE /api/orgs/{id}` does. The guard judges the SET: it refuses (409) any deletion that would leave no live holder of the `PFF_SUPERADMIN` role, including one where the orgs are each safe to delete alone but hold every superadmin between them. `?dryRun=true` answers the same shape with no write — and a refused plan is then a 200 with `blocked: true` and the reason, which is how the portal shows it before confirming. Requires `execute:DeleteOrgCommand`. The caller's own token is forwarded. Registered BEFORE `DELETE /api/orgs/{id}`, so the literal `bulk` segment is not captured by the param route.\n\nThis changes data. The person is asked to confirm it before it runs, and nothing happens if they decline.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "dryRun": {
        "type": "string",
        "description": "The string \"true\" previews the deletion without performing it. Compared as a string, so `?dryRun=1` deletes."
      },
      "body": {
        "type": "string",
        "description": "The JSON request body: { orgIds*: array of string }"
      }
    },
    "required": [
      "body"
    ]
  },
  "annotations": {
    "readOnlyHint": false,
    "destructiveHint": true,
    "idempotentHint": true,
    "openWorldHint": false
  }
}