Skip to content
POST /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

idstring
required·

Access request id (areq_…).

Request Body

decisionstring
Possible values:
approveddenied
expires_atstring,null · date-time

When the grant this approval writes auto-expires. null/omitted means no expiry. Ignored unless this approval actually writes a grant.

notestring
remove_member_rulesstring[]

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

resolutionstring

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.

Possible values:
grantpolicy

Response Body

can_resolveboolean

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

can_withdrawboolean

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.

connector_labelstring

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.

connector_logostring,null

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.

connector_restricted_for_requesterboolean

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.

connector_slugstring,null

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.

created_atstring · date-time
idstring

Access request id (areq_…).

inherited_from_connector_labelstring,null

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.

inherited_from_member_ruleboolean

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

notestring,null

Optional free text from the requester, ≤ 2000 characters.

permissionstring,null

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.

reasonstring

What kind of refusal this request is asking to be reconsidered.

Possible values:
permissionrestriction
requesterobject

Resolved member profile. Falls back to { id } alone when the profile row no longer resolves.

avatar_urlstring,null

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.

emailstring · email
idstring

User id (usr_…).

namestring,null
requester_user_idstring

User id (usr_…) of whoever filed the request.

resolution_notestring,null
resolved_atstring,null · date-time
resolved_byobject,null

Resolved member profile. Falls back to { id } alone when the profile row no longer resolves.

avatar_urlstring,null

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.

emailstring · email
idstring

User id (usr_…).

namestring,null
resolved_by_user_idstring,null

User id (usr_…) of the admin who resolved it.

resource_typestring
Possible values:
toolconnector
statusstring
Possible values:
pendingapproveddeniedwithdrawn
toolstring

What was asked for: a tool name, or a connector slug when resource_type is "connector".

updated_atstring · date-time
will_lift_restrictionboolean

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())