Resolve someone else’s access request
/access-request/{id}/resolve
**Approving a `restriction`-reason request with `resolution: "grant"` (the default) lifts that restriction for the requester alone** — a whole connector, or one tool on the connector a tool request names in `connector_slug` — by writing a first-class ACCESS GRANT (who, what, approved by, when, and optionally an expiry). The requester’s own member-level rules are edited (and a block rule the approval empties is removed); where their ROLE’s rules stand in the way, the grant is what lifts them. Role rules themselves are never edited or copied, so every other role rule — including one added later — keeps binding the requester. A tool on a connector blocked whole cannot be carved out and is not lifted. `resolution: "policy"` instead records the decision WITHOUT writing a grant — the admin is fixing the underlying role/connector rule directly through the restriction APIs. If the requester has a personal rule that would need editing to open the target, an approval that lists `remove_member_rules` (an array of the personal rule ids the caller confirms editing) must list EXACTLY the conflicting ids or the call answers `409 member_rule_conflict` naming them in `error.details.member_rule_conflicts`. OMITTING `remove_member_rules` altogether skips that check and edits the personal rules as before — a deliberate, temporary compatibility path for clients that predate the field, to be tightened once the console confirms it. `expires_at` (ISO, future, at most ~10 years out; any UTC offset is accepted and stored normalised to UTC) sets when a grant auto-expires; omit or send `null` for no expiry. `will_lift_restriction` on the admin view previews whether approving will change anything; when it is `false` for a fork because a rule written for the requester personally blocks the connector it was forked from, `inherited_from_member_rule` says so (approving never overrides a personal rule). Every other approval — a `permission`-reason request, or a tool request with no `connector_slug` — records a decision only; widening access there is a separate act through `PATCH /member/{userId}` or the restriction APIs. Requires `member:manage`. Refuses with `409` when the request has already left `pending` — a decision is recorded once. ONLY AN INTERACTIVE BROWSER SESSION can decide: an organization API token (`Bearer elch_…`) is refused with `403`, however many permissions it holds, because approving an access request is a person’s decision and a token has no person behind it. The session must also 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. A governance decision is recorded once and never re-opened, so the person recording it re-proves they are still the person holding the session. **An admin may not decide their own request: `403`**, whichever decision they send and however many permissions they hold — self-approval of a restriction request would be a self-grant, and beyond that the record is the product: "approved by Roopi" on Roopi’s own ask reads downstream exactly like an approval a second person signed. Enforced on the route, not only by `can_resolve`, so a client that ignores the capability field is refused rather than obeyed. The requesting admin keeps `can_withdraw` on that row instead.
Path Parameters
Access request id (areq_…).
Request Body
approveddenied
When the grant this approval writes auto-expires. null/omitted means no expiry. Ignored unless this approval actually writes a grant.
The ids of the personal rules of the requester that this approval would edit, as the caller confirms them: must equal the conflicting set EXACTLY (an empty array asserts there are none), or the call answers 409 member_rule_conflict with the real ids in error.details. Omit the field to skip the check (legacy behaviour: the rules are edited without confirmation).
Only meaningful on an APPROVED, reason: "restriction" request. grant (default) writes an access grant; policy approves without writing one — the admin is changing the underlying rule directly instead.
grantpolicy
Response Body
True while status is still pending and the row is not the caller’s own — the two preconditions resolve itself enforces (409 and 403 respectively).
True when the caller is the requester and status is still pending. Present on this shape as well as on the self view: an admin’s own request sits in their own queue, and withdrawing it is the one verb on that row that is theirs.
Display name of connector_slug (or, on a "connector" request, of tool itself) — resolved from the catalog in the same batch as tool_label, falling back to the slug. Present on every view: the requester reads it in the self view exactly as an admin does in the queue, so it lives here rather than only on the admin shape.
The connector’s mark for the same slug connector_label names — the icon-first, logo-second image the Connectors page and every other connector picture in the console already use. null when the catalog has no mark for the slug; present under the same conditions as connector_label.
On the same rows, for a TOOL request only: true when approving lifts nothing because the requester’s WHOLE connector is restricted — one tool cannot be carved out of a connector restricted whole, and an approval never opens the whole connector for a one-tool ask. Their connector access is what an admin would have to grant instead.
On a "tool" request, the connector the tool lives on when the request named one — what makes approving it able to lift the restriction on that tool. Always null on a "connector" request.
Access request id (areq_…).
With inherited_from_member_rule: true: the display name of the connector it was forked from, or null when it cannot be named to this caller (private to someone else, deleted, or the catalog did not answer). Never a slug or id.
On the same rows, for a CONNECTOR request only: true when approving lifts nothing because the connector is a fork and a restriction written for the requester personally blocks the connector it was forked from. A fork inherits its original’s block (forking must not evade one) and an approval never overrides a rule written about one person, so the fix is to approve their access to the original or edit their restrictions. A block inherited through the requester’s ROLE is not this: approving the fork lifts it (will_lift_restriction: true).
Optional free text from the requester, ≤ 2000 characters.
Present only when reason is "permission" — never populated for a "restriction"-reason request, on either write or read. That is the disclosure rule: a restriction refusal never names the rule that blocked the caller, so this field must not become a second channel for the same fact.
What kind of refusal this request is asking to be reconsidered.
permissionrestriction
Resolved member profile. Falls back to { id } alone when the profile row no longer resolves.
The person's picture — the same one the console's user menu draws: their stored profile picture, else a Gravatar URL (d=404, 96px) derived server-side from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load.
User id (usr_…).
User id (usr_…) of whoever filed the request.
Resolved member profile. Falls back to { id } alone when the profile row no longer resolves.
The person's picture — the same one the console's user menu draws: their stored profile picture, else a Gravatar URL (d=404, 96px) derived server-side from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load.
User id (usr_…).
User id (usr_…) of the admin who resolved it.
toolconnector
pendingapproveddeniedwithdrawn
What was asked for: a tool name, or a connector slug when resource_type is "connector".
On a pending restriction-reason request approving could act on (a connector request, or a tool request naming its connector_slug): whether approving it would actually lift anything. Absent on every other row.
curl -X POST 'https://api.elaichi.ai/access-request/<id>/resolve' \
-H 'Authorization: Bearer $ELAICHI_API_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"decision":"approved","note":"your_note","resolution":"grant","remove_member_rules":[]}'const body = {
"decision": "approved",
"note": "your_note",
"resolution": "grant",
"remove_member_rules": []
};
const response = await fetch('https://api.elaichi.ai/access-request/<id>/resolve', {
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/access-request/<id>/resolve"
headers = {
"Authorization": f"Bearer {os.environ['ELAICHI_API_TOKEN']}",
"Content-Type": "application/json",
}
payload = {
"decision": "approved",
"note": "your_note",
"resolution": "grant",
"remove_member_rules": []
}
response = requests.post(url, headers=headers, json=payload)
print(response.json())