Remove member
/member/{userId}
Irreversible: revokes the member's access to this organization immediately. Re-adding them means a fresh invite, and their roles and team memberships are not restored. Their connections are ALWAYS deleted (the vaulted credential with them); a `connection_actions` entry saying `transfer` is refused with 400 `connection_transfer_removed`. To keep what used a connection working, name a replacement in `connection_replacements`: before the old connection is deleted, every toolbox entry and other member's synthetic tool step that names it is rewritten onto the replacement (toolbox entries are re-stamped with you as delegator). A replacement must be the same connector, active, not the leaver's, and usable by you (400 `replacement_*` otherwise) — the preflight's `replacement_candidates` are exactly those. Everything else they own goes to a person (`target_user_id`) or a team (`target_team_id`: the team's first active admin owns it and the team gets an `edit` share; 400 `team_has_no_admin` when nobody can) per row, or to one bulk `defaults.transfer_to_user_id` / `transfer_to_team_id`. A shared owned resource with no decision refuses with 409 `<kind>_need_resolution`. Requires `member:manage`. A BROWSER SESSION must additionally carry a fresh step-up reauthentication (`X-Step-Up-Token`, obtained from `/auth/step-up`); without one the call answers `428 step_up_required` and `error.details` names the action and resource to prove. An organization API token is not challenged: step-up re-proves the person behind a session, and a token has no person behind it. The gate is demanded LAST, after every resolution refusal above, so an admin never re-authenticates only to be told the removal needs decisions first. Custom connectors the member owns are resolved by `connector_actions` (`transfer` only — never delete, and never released: an ownerless connector is a legacy state). EVERY owned connector, private or shared, needs a new owner: one with no entry is refused with 409 `connectors_need_resolution` unless `defaults.transfer_to_user_id` names a bulk recipient. A body that still sends `action: "release"` or `defaults.release_connectors` is refused with 400 `connector_release_removed` (the message says what to send instead). Should the member still own a connector when the removal reaches the org itself, that is 409 `member_owns_connectors`. The response `applied` counts include `connectors`.
Path Parameters
User id (usr_…) of an org member.
Request Body
Per-resource decisions; a shared one with none (and no bulk target) refuses the removal.
transferdelete
Id from the offboarding preflight.
A team (team_…): its first active admin owns it, the team gets an edit share. Never with target_user_id.
A person (usr_…): an active member other than the leaver and the caller.
Per-resource decisions; a shared one with none (and no bulk target) refuses the removal.
transferdelete
Id from the offboarding preflight.
A team (team_…): its first active admin owns it, the team gets an edit share. Never with target_user_id.
A person (usr_…): an active member other than the leaver and the caller.
Optional: every connection the member owns is deleted whether named or not.
The only action. transfer answers 400 connection_transfer_removed.
delete
Connection id (conn_…) from the offboarding preflight.
Before each old connection is deleted, rewrite every toolbox entry and other member's synthetic tool step naming it onto the replacement. One entry per old connection.
The leaver's connection (conn_…).
Same connector, active, not the leaver's, usable by the caller. null keeps this one connection out of its app's replacement_defaults entry: the tools that used it stop.
One decision per custom connector the member owns (see connectors on the offboarding preflight). Required for every connector unless defaults.transfer_to_user_id covers it.
transfer hands ownership to target_user_id (grants untouched). There is no delete, and no release: release was removed and answers 400 connector_release_removed.
transfer
Connector slug from the offboarding preflight.
For transfer: a team (team_…) instead of a person.
For transfer (usr_…) — an active member other than the one being removed and other than the caller.
Per-resource decisions; a shared one with none (and no bulk target) refuses the removal.
transferdelete
Id from the offboarding preflight.
A team (team_…): its first active admin owns it, the team gets an edit share. Never with target_user_id.
A person (usr_…): an active member other than the leaver and the caller.
Bulk decisions for every resource that carries no explicit per-resource action.
Acknowledge, once, that every synthetic tool is deleted with its author.
Or one team (team_…): its first active admin owns everything, the team gets an edit share. Never with transfer_to_user_id.
One active member (usr_…) who inherits everything owned that has no explicit action. Connections never follow it.
One decision per KIND for every item of it with no per-resource entry. Per item: its own entry wins, then its kind's default, then defaults. A transfer names exactly one of target_user_id / target_team_id; a delete names neither (400 otherwise). Unlike defaults, a kind default may delete — it names one kind deliberately — and a delete reaches only the items in the summary the caller read: one created since is left for the next round (202).
3 properties
transferdelete
For transfer: a team (team_…) instead of a person.
For transfer: a person (usr_…).
3 properties
Custom connectors are never deleted.
transfer
A team (team_…) instead of a person.
A person (usr_…).
3 properties
transferdelete
For transfer: a team (team_…) instead of a person.
For transfer: a person (usr_…).
3 properties
transferdelete
For transfer: a team (team_…) instead of a person.
For transfer: a person (usr_…).
3 properties
transferdelete
For transfer: a team (team_…) instead of a person.
For transfer: a person (usr_…).
3 properties
transferdelete
For transfer: a team (team_…) instead of a person.
For transfer: a person (usr_…).
Per-resource decisions; a shared one with none (and no bulk target) refuses the removal.
transferdelete
Id from the offboarding preflight.
A team (team_…): its first active admin owns it, the team gets an edit share. Never with target_user_id.
A person (usr_…): an active member other than the leaver and the caller.
One replacement per app instead of one connection_replacements entry per connection: every connection of connector_slug with no connection_replacements entry of its own is replaced by replacement_connection_id, validated per connection exactly like an explicit entry. A replacement the rules refuse is a 400 for the whole request (replacement_*); an app named twice is 400 duplicate_replacement_default; an app the member has no connection of is 400 replacement_default_unknown_connector.
The app, from the preflight's connections.
One of that app's replacement_candidates.
Per-resource decisions; a shared one with none (and no bulk target) refuses the removal.
transferdelete
Id from the offboarding preflight.
A team (team_…): its first active admin owns it, the team gets an edit share. Never with target_user_id.
A person (usr_…): an active member other than the leaver and the caller.
Per-resource decisions; a shared one with none (and no bulk target) refuses the removal.
transferdelete
Id from the offboarding preflight.
A team (team_…): its first active admin owns it, the team gets an edit share. Never with target_user_id.
A person (usr_…): an active member other than the leaver and the caller.
Response Body
curl -X DELETE 'https://api.elaichi.ai/member/<userId>' \
-H 'Authorization: Bearer $ELAICHI_API_TOKEN' \
-H 'Content-Type: application/json'const response = await fetch('https://api.elaichi.ai/member/<userId>', {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + process.env.ELAICHI_API_TOKEN,
'Content-Type': 'application/json',
},
});
const data = await response.json();
console.log(data);import os
import requests
url = "https://api.elaichi.ai/member/<userId>"
headers = {
"Authorization": f"Bearer {os.environ['ELAICHI_API_TOKEN']}",
"Content-Type": "application/json",
}
response = requests.delete(url, headers=headers)
print(response.json())