---
title: "File an access request"
method: POST
path: "/access-request"
tags: ["Access requests"]
---

# File an access request

`POST /access-request`

## Request

- Base URL: `https://api.elaichi.ai`
- URL: `https://api.elaichi.ai/access-request`
- Auth: HTTP bearer

Files a request for access. Any member may call it, with no permission required, because asking for a permission must not itself need one. A request is always filed as the caller, so the body carries no requester. `permission` is required and must be a known permission name when `reason` is `"permission"`, and forbidden (`400`) when `reason` is `"restriction"`: a restriction refusal never names the rule that blocked someone, and this resource must not become a second way to learn it. `reason` is never `"scope"`, because a scope refusal has no admin-side fix.

**Filing never grants anything.** It creates a record for an admin to decide; approving a `restriction`-reason request is what can lift the restriction (see `POST /access-request/{id}/resolve`).

**De-duplicated.** A second request for the same `tool` while an earlier one from the same requester is still `pending` returns the existing row (updating `note` when a different one was sent) with `200`. A genuinely new row is `201`.

**A tool request that could never be granted is refused.** A `restriction`-reason `"tool"` request naming a `connector_slug` whose whole connector is restricted for the caller is `409 tool_request_connector_blocked`: approving it would lift nothing, because one tool cannot be carved out of a connector restricted whole. The message names the connector and says to ask for it instead; `error.details` carries `connector_slug`, `connector_label` and `pending_request` (the caller’s own open connector request, or null). Every tool row that can draw an ask carries `restricted_scope` (`"connector"`, `"tool"` or null), so a client can file the connector request up front.

## Headers

- `X-Organization-Id` string

## Request body

- object
  - `tool` string, required: The tool name the caller was blocked from calling, or a connector slug when `resource_type` is `"connector"`.
  - `resource_type` 'tool' | 'connector'
  - `connector_slug` string: Only on a `"tool"` request (400 on a connector request): the connector the tool lives on. Lets an approval lift the restriction on exactly that tool; without it approving records a decision only.
  - `reason` 'permission' | 'restriction', required
  - `permission` string: Required when `reason` is `"permission"`; forbidden (400) when `reason` is `"restriction"`.
  - `note` string

## Response `200`

De-duplicated onto an existing pending request

- object: The requester’s own view, returned by `POST /access-request`, `POST /access-request/{id}/withdraw`, and `GET /access-request/{id}` when the caller is the request’s own requester. `resolved_by_user_id` stays a bare id here, because resolving it into a name and email is a member-directory lookup this view does not hand a plain member.
  - `id` string: Access request id (`areq_…`).
  - `requester_user_id` string: User id (`usr_…`) of whoever filed the request.
  - `tool` string: What was asked for: a tool name, or a connector slug when `resource_type` is `"connector"`.
  - `resource_type` 'tool' | 'connector'
  - `connector_slug` string, nullable: 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.
  - `connector_label` string: Display name of `connector_slug` (or, on a `"connector"` request, of `tool` itself), 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.
  - `connector_logo` string, nullable: The connector’s logo for the slug `connector_label` names. `null` when the catalog has no logo for the slug; present under the same conditions as `connector_label`.
  - `reason` 'permission' | 'restriction': What kind of refusal this request is asking to be reconsidered.
  - `permission` string, nullable: 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.
  - `note` string, nullable: Optional free text from the requester, ≤ 2000 characters.
  - `status` 'pending' | 'approved' | 'denied' | 'withdrawn'
  - `created_at` string, date-time
  - `resolved_at` string, date-time, nullable
  - `resolved_by_user_id` string, nullable: User id (`usr_…`) of the admin who resolved it.
  - `resolution_note` string, nullable
  - `restriction_lifted` boolean, nullable: What an approval did to the requester’s access, on every view: `true` when it wrote a grant, `false` when the request was approved and nothing was lifted (a `resolution: "policy"` approval, access that was already open, or a shape no grant can open), `null` when the question does not apply (not approved, a `permission` request, a tool request with no `connector_slug`). It never says why; the connector’s own `restricted`, `restricted_by` and `restricted_inherited_from` do, within what the caller may be told. An older approval may read `false` for access it did give, so read the connector’s `restricted` for the live answer. It is the outcome at resolve time: a later revoke, expiry or replacement does not change it.
  - `updated_at` string, date-time
  - `can_withdraw` boolean: True when the caller is the requester and `status` is still `pending`.

## Other responses

- `201`: Created a new request
- `400`: Bad request
- `401`: Unauthorized
- `403`: Forbidden
- `404`: Not found
- `429`: Rate limited

---

[API](https://skmtc.dev/elaichi/apis/elaichi-api.md) · [All operations](https://skmtc.dev/elaichi/apis/elaichi-api/llms.txt) · [OpenAPI document](https://skmtc.dev/elaichi/apis/elaichi-api/revisions/b22cfb7b6d15?raw)
