Update a restriction
/restriction/{id}
Merge-patch at the top level, but **`connector_slugs` and `tools` are each REPLACED wholesale when present** — send the complete list, since anything omitted is dropped from the rule. Same permissions as create, including `restriction:override` for user targets. The connector/tool overlap rule from `POST` applies to the resulting rule, with one exemption: an overlap the row ALREADY had is left alone, so a row written before the rule existed stays editable through a rename, a mode flip or a target change that echoes both arrays back. Only overlap this patch ADDS is a `400`.
Path Parameters
Restriction id (rstr_…).
Request Body
Replaces the list.
false clears a legacy override on every row of that member, so their role rules reach them again beside their own. Only ever narrows them. The flag cannot be set.
false
allowblockexcept
roleuser
Replaces the list.
Connector slug — connectors are keyed by slug, not by id.
Exact tool name from GET /restriction/connector/{slug}/tools.
Response Body
The access request (areq_…) whose approval wrote this except row; null otherwise.
Whether the caller may create, edit or delete THIS row — restriction:manage, plus the two guards the write routes themselves enforce: a user-targeted row additionally needs restriction:override, and a row targeting the caller (or a role whose permission set equals theirs exactly) is refused so nobody can free themselves from a restriction. Computed by calling those same guard functions, never re-derived, so a row showing can_manage: true cannot 403 on write. Read routes need only restriction:view, so this is false for a viewer.
Whole connectors covered by the rule. A slug here settles every tool on that connector, so it must not also appear as a tools[].connector_slug.
User id (usr_…) of the author.
Display name (or email) of the author, resolved server-side in the SAME batch as target_label — one query for the whole page, so no client ever looks an author up row by row. null while created_by is a real id means the author has LEFT the org (the resolver joins through the org's own member index), which is a fact about the person, not a lookup that failed — render it as "Former member", the same word GET /audit-log's actor summary uses. Also null when created_by itself is.
Restriction id (rstr_…).
True on a member row that still REPLACES its member's role rules — the meaning every member row had before role and member rules combined, kept on the rows whose conversion could not be proven to leave the member's access unchanged (a role allowlist beside member rows with none). While any of a member's rows carries it, no role rule reaches them, including ones added later. Read-only except that PATCH may send false to clear it on all of that member's rows, which only ever narrows them.
allow makes the listed connectors and tools the ONLY ones the target may use; block removes exactly the listed ones and leaves everything else reachable. The allowlist engages on the MODE, not on what the rule names: an allow rule with empty connector_slugs and empty tools blocks every connector and every tool for its target. except (member targets only) is an EXCEPTION: it lifts, for that one member, the role rules that name exactly these connectors or tools, and nothing else — never a rule written for the member personally, never another connector, and never a whole-connector role block for one of its tools. A member is bound by their role's rules and their own allow/block rules at once; a connector or tool is reachable only when both admit it.
allowblockexcept
Where an except row came from: access_request when approving a request wrote it (access_request_id names the request), migration when the move to combined role and member rules wrote it to keep a member's access exactly what it was. null on a row an admin wrote by hand.
access_requestmigrationnull
One plain sentence saying what this rule does ("Allows botify, plus 2 tools in slack. Everything else is blocked."), computed server-side so every client says the same thing. Read-only, derived from mode/connector_slugs/tools, never stored. It describes what is ENFORCED: tool entries on a connector already named whole are inert, so they do not appear in it. Connectors are named by slug — resolving a display name would be one catalog read per slug per row. Render this rather than composing your own; a client-derived summary is how "an allow rule that names nothing" came to read as "All connectors".
Role id (role_…) or user id (usr_…), matching target_type.
Display name (or email) of a user target, resolved server-side one batch per page so the console never looks members up row by row. Always null on a role target — role names live in the org DO, which that batch cannot reach, so the console resolves them against the role catalog it already holds — and null on a stale target whose member has been deleted.
Whether this rule applies to everyone holding a role, or to one specific member.
roleuser
Individual tools covered by the rule, on connectors connector_slugs does not already name whole.
Connector slug — connectors are keyed by slug, not by id.
Person-readable phrase for this tool ("create a contact"), computed server-side from tool_name. Read-only. Render this instead of the raw id, and never rebuild it client-side — the humanizer lives on the server and a second copy drifts from it. The connector is deliberately not folded in: it is right beside this as connector_slug.
Exact tool name from GET /restriction/connector/{slug}/tools.
User id (usr_…) of whoever wrote the row LAST — the author until somebody else edits it. Null only on a row written before this field existed; the self-target rule reads that as "not you", so such a row constrains its author like anyone else's.
Display name (or email) for updated_by, resolved in the same one batch as created_by_label, with the same "Former member" meaning for a null beside a real id. Compare the two IDS, never the two labels, to decide whether somebody other than the author last wrote the row: two people can share a display name, and both labels are null once both have left.
curl -X PATCH 'https://api.elaichi.ai/restriction/<id>' \
-H 'Authorization: Bearer $ELAICHI_API_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"target_type":"role","target_id":"your_target_id","mode":"allow","connector_slugs":[],"tools":[],"legacy_override":false}'const body = {
"target_type": "role",
"target_id": "your_target_id",
"mode": "allow",
"connector_slugs": [],
"tools": [],
"legacy_override": false
};
const response = await fetch('https://api.elaichi.ai/restriction/<id>', {
method: 'PATCH',
headers: {
'Authorization': 'Bearer ' + process.env.ELAICHI_API_TOKEN,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
const data = await response.json();
console.log(data);import os
import requests
url = "https://api.elaichi.ai/restriction/<id>"
headers = {
"Authorization": f"Bearer {os.environ['ELAICHI_API_TOKEN']}",
"Content-Type": "application/json",
}
payload = {
"target_type": "role",
"target_id": "your_target_id",
"mode": "allow",
"connector_slugs": [],
"tools": [],
"legacy_override": False
}
response = requests.patch(url, headers=headers, json=payload)
print(response.json())