Skip to content
POST /restriction

Requires `restriction:manage` and the `restrictions` plan feature. A `user` target additionally requires `restriction:override`. A member is bound by their role's rules AND their own at once: a member `allow`/`block` rule only ever narrows, and the one way to loosen a role rule for one person is an `except` rule, which lifts only the role rules naming exactly what it names. `except` is `400` on a `role` target, and `403` aimed at yourself — always, even when nothing restricts you, because an exception written before a role rule exists would exempt you from it later. Changes take effect within about a minute, once the org rule cache turns over. **A connector named in `connector_slugs` must not also appear as a `tools[].connector_slug`** — `400`, naming the connector. Restricting a whole connector already settles every tool on it, so the tool entries change nothing; on an `allow` rule they are worse than nothing, because the rule then grants the WHOLE connector rather than the tools that were picked. Read `summary` back to see what the rule you just wrote actually does.

Request Body

connector_slugsstring[]

Whole connectors the rule covers. Defaults to empty — which on an allow rule with no tools either means the rule blocks EVERY connector and tool for its target, not that it allows them all. The allowlist engages on mode, not on what the rule names.

modestring
Possible values:
allowblockexcept
target_idstring

Role id (role_…) or user id (usr_…).

target_typestring
Possible values:
roleuser
toolsobject[]

Individual tools the rule covers. Defaults to empty. A connector_slug here must not also be in connector_slugs.

connector_slugstring

Connector slug — connectors are keyed by slug, not by id.

tool_namestring

Exact tool name from GET /restriction/connector/{slug}/tools.

Response Body

access_request_idstring,null

The access request (areq_…) whose approval wrote this except row; null otherwise.

can_manageboolean

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.

connector_slugsstring[]

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.

created_atstring · date-time
created_bystring,null

User id (usr_…) of the author.

created_by_labelstring,null

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.

idstring

Restriction id (rstr_…).

legacy_overrideboolean

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.

modestring

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.

Possible values:
allowblockexcept
originstring,null

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.

Possible values:
access_requestmigrationnull
summarystring

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".

target_idstring

Role id (role_…) or user id (usr_…), matching target_type.

target_labelstring,null

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.

target_typestring

Whether this rule applies to everyone holding a role, or to one specific member.

Possible values:
roleuser
toolsobject[]

Individual tools covered by the rule, on connectors connector_slugs does not already name whole.

connector_slugstring

Connector slug — connectors are keyed by slug, not by id.

tool_labelstring

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.

tool_namestring

Exact tool name from GET /restriction/connector/{slug}/tools.

updated_atstring · date-time
updated_bystring,null

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.

updated_by_labelstring,null

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 POST 'https://api.elaichi.ai/restriction' \
  -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":[]}'
const body = {
  "target_type": "role",
  "target_id": "your_target_id",
  "mode": "allow",
  "connector_slugs": [],
  "tools": []
};

const response = await fetch('https://api.elaichi.ai/restriction', {
  method: 'POST',
  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"
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": []
}

response = requests.post(url, headers=headers, json=payload)
print(response.json())