---
title: Just-in-time access and approvals
description: Request, approve and track temporary access to a role, and run
  multi-approver approval workflows. Records only for now.
url: https://hexelstudio.com/docs/identity/just-in-time-access
---

<Warning>
Just-in-time (JIT) requests and approval workflows are records only. An `APPROVED` or `ACTIVE` JIT request does not give the requester the role's permissions and does not let them assume the role, and moving a request to `REVOKED` or `EXPIRED` removes nothing. Nothing moves a request to `EXPIRED` when its time runs out. An approved approval request does not carry out the change it describes, and workflows are not connected to JIT requests. Use these APIs to keep an auditable record of requests and decisions, and grant the access itself as described in [Roles](https://hexelstudio.com/docs/identity/roles). This API may change.
</Warning>

## What works today

- Requests and workflows are stored in your organization, with who did what and when.
- The rules about who may decide are enforced: nobody approves their own JIT request or decides their own approval request, approvers must hold the permission and, for workflows, the approver list and approver role.
- The JIT state machine is enforced: a status change that is not allowed answers `409`.

To give temporary access today, add the person to a role's trust policy and let them assume the role. A role session ends when it expires, and you can end every session of a role at once. See [Roles](https://hexelstudio.com/docs/identity/roles).

## JIT access requests

A JIT request asks for a role, optionally in one workspace or environment, for a number of minutes. The requester is always the caller, in the caller's organization.

### Permissions

| Action | What it allows |
| --- | --- |
| `iam:jit:write` | Create requests. Activate or revoke your own approved request. |
| `iam:jit:approve` | Approve or reject other people's requests. Activate, revoke or expire any request. |
| `iam:jit:read` | Read every request of the organization. Without it you see only your own. |

### Create a request

`POST /v1/jit` needs `iam:jit:write`.

| Field | Type | Description |
| --- | --- | --- |
| `role_id` | string | **Required.** An existing role available to your organization. Otherwise `400` (`role not found`). |
| `workspace_id` | string | Optional workspace the access is for. |
| `environment_id` | string | Optional environment. Needs `workspace_id`. |
| `reason` | string | Why the access is needed. |
| `ticket_id` | string | Optional reference, such as an incident number. |
| `duration_minutes` | integer | How long the access should last once active. |

```bash
curl -X POST https://api.hexelstudio.com/v1/jit \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "role_id": "<role-id>",
    "reason": "Investigate failed deployment",
    "ticket_id": "INC-1234",
    "duration_minutes": 60
  }'
```

The response is `201 Created`:

```json
{
  "id": "jit_...",
  "user_id": "<user-id>",
  "role_id": "<role-id>",
  "organization_id": "<org-id>",
  "reason": "Investigate failed deployment",
  "ticket_id": "INC-1234",
  "duration_minutes": 60,
  "status": "PENDING",
  "created_at": "2026-10-20T09:00:00Z"
}
```

`user_id` and `organization_id` come from your token. `status` always starts as `PENDING`. As the request moves on, the response also carries `approved_by_user_id`, `approved_at`, `activated_at`, `expires_at`, `revoked_at` and `revoked_by_user_id`.

### Change the status

`PATCH /v1/jit/{request_id}/status` with `{"status": "..."}`.

<JitStatesDiagram />

| New status | Who may set it | What is recorded |
| --- | --- | --- |
| `APPROVED`, `REJECTED` | `iam:jit:approve`, never on your own request | `APPROVED` sets `approved_by_user_id` and `approved_at` |
| `ACTIVE` | The requester with `iam:jit:write`, or anyone with `iam:jit:approve` | `activated_at`, and `expires_at` = now + `duration_minutes` |
| `REVOKED` | The requester with `iam:jit:write`, or anyone with `iam:jit:approve` | `revoked_at` and `revoked_by_user_id` |
| `EXPIRED` | `iam:jit:approve` | Status only |

`REJECTED`, `REVOKED` and `EXPIRED` are final.

```bash
curl -X PATCH https://api.hexelstudio.com/v1/jit/<request-id>/status \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status": "APPROVED"}'
```

```json
{"status": "APPROVED"}
```

| Status | When |
| --- | --- |
| `400` | `status` is not one of the six statuses. |
| `403` | You lack the permission for this change, or you are approving or rejecting your own request (`a requester cannot approve or reject its own request`). |
| `404` | No such request in your organization. |
| `409` | The change is not allowed from the current status, or the request changed at the same time. The body names `from` and `to`. |

```json
{"error": "status change not allowed", "from": "PENDING", "to": "ACTIVE"}
```

<Note>
`expires_at` is informational. When it passes, the request stays `ACTIVE` until someone with `iam:jit:approve` sets it to `EXPIRED`.
</Note>

### Read requests

| Call | Permission | Response |
| --- | --- | --- |
| `GET /v1/jit` | No specific action. With `iam:jit:read` you see every request, without it only your own. | Array of requests, newest first. Filter with `?status=`. |
| `GET /v1/jit/{request_id}` | `iam:jit:read`, or being the requester | The request, or `404` |

```bash
curl "https://api.hexelstudio.com/v1/jit?status=PENDING" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

## Approval workflows

An approval workflow defines who must approve a type of change and how many approvals it needs. Approval requests are filed against a workflow and settled by approvers' decisions.

### Permissions

| Action | What it allows |
| --- | --- |
| `iam:approval:read` | Read workflows, requests and decisions. |
| `iam:approval:write` | Create, change and delete workflows, and create requests. |
| `iam:approval:manage` | Decide requests. |

### Workflows

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Name of the workflow. |
| `description` | string | Optional. |
| `workflow_type` | string | `ROLE_ASSIGNMENT`, `JIT_ACCESS`, `POLICY_CHANGE` or `SERVICE_USER_CREATE`. A label: it does not connect the workflow to that feature. |
| `scope_type`, `scope_id` | string | Optional labels for what the workflow covers. |
| `requires_approval` | boolean | Stored. Default `true`. |
| `approver_role` | string | Optional. `org_owner`, `org_admin`, `org_member`, `org_viewer`, or the ID of a permission set. Any other value answers `400`. |
| `approver_user_ids` | array of strings | Optional. When set, only these users may decide. |
| `min_approvals` | integer | Distinct approvals needed. Default `1`. |
| `is_enabled` | boolean | Stored. Default `true`. A disabled workflow still accepts requests and decisions. |

The response adds `id` (`aw_...`), `organization_id`, `created_at` and `updated_at`.

```bash
curl -X POST https://api.hexelstudio.com/v1/approvals/workflows \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production policy changes",
    "workflow_type": "POLICY_CHANGE",
    "approver_role": "org_admin",
    "min_approvals": 2
  }'
```

| Call | Permission | Response |
| --- | --- | --- |
| `POST /v1/approvals/workflows` | `iam:approval:write` | `201` with the workflow |
| `GET /v1/approvals/workflows` | `iam:approval:read` | Array of your organization's workflows |
| `GET /v1/approvals/workflows/{workflow_id}` | `iam:approval:read` | The workflow, or `404` |
| `PATCH /v1/approvals/workflows/{workflow_id}` | `iam:approval:write` | The updated workflow. `id`, `organization_id` and timestamps never change; `"approver_role": ""` clears the role. |
| `DELETE /v1/approvals/workflows/{workflow_id}` | `iam:approval:write` | `204`. The workflow's requests and decisions are deleted with it. |

### Requests

| Field | Type | Description |
| --- | --- | --- |
| `workflow_id` | string | **Required.** A workflow of your organization, otherwise `404`. |
| `request_type` | string | What is being requested, in your own terms. |
| `request_data` | object | Details of the request. Stored as given. |
| `reason` | string | Optional. |
| `expires_at` | string | Optional timestamp. Stored only: requests do not expire. |

The requester is the caller. `status` starts as `PENDING` and becomes `APPROVED` or `REJECTED` through decisions.

```bash
curl -X POST https://api.hexelstudio.com/v1/approvals/requests \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "workflow_id": "<workflow-id>",
    "request_type": "attach_policy",
    "request_data": {"policy_id": "<policy-id>", "target_type": "GROUP", "target_id": "<group-id>"},
    "reason": "Give the on-call group read access"
  }'
```

```json
{
  "id": "areq_...",
  "workflow_id": "<workflow-id>",
  "requester_user_id": "<user-id>",
  "request_type": "attach_policy",
  "request_data": {"policy_id": "<policy-id>", "target_type": "GROUP", "target_id": "<group-id>"},
  "status": "PENDING",
  "reason": "Give the on-call group read access",
  "created_at": "2026-10-20T09:00:00Z",
  "updated_at": "2026-10-20T09:00:00Z"
}
```

| Call | Permission | Response |
| --- | --- | --- |
| `POST /v1/approvals/requests` | `iam:approval:write` | `201` with the request |
| `GET /v1/approvals/requests` | `iam:approval:read` | Array of your organization's requests, newest first. Filter with `?workflow_id=` and `?status=`. |
| `GET /v1/approvals/requests/{request_id}` | `iam:approval:read` | The request, or `404` |

### Decide a request

`POST /v1/approvals/requests/{request_id}/decisions` needs `iam:approval:manage`.

| Field | Type | Description |
| --- | --- | --- |
| `decision` | string | **Required.** `APPROVED` or `REJECTED`. |
| `comment` | string | Optional. |

```bash
curl -X POST https://api.hexelstudio.com/v1/approvals/requests/<request-id>/decisions \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"decision": "APPROVED", "comment": "Looks good"}'
```

```json
{
  "id": "ad_...",
  "request_id": "<request-id>",
  "approver_user_id": "<user-id>",
  "decision": "APPROVED",
  "comment": "Looks good",
  "decided_at": "2026-10-20T09:05:00Z"
}
```

The approver is the caller. A decision is accepted only when:

- the request is still `PENDING` (otherwise `409`, `approval request is no longer pending`);
- the caller is not the requester (otherwise `403`);
- the caller is listed in `approver_user_ids`, when the workflow sets it (otherwise `403`);
- the caller holds `approver_role`, when the workflow sets it (otherwise `403`). A membership level counts for that level and every higher one (`org_owner` > `org_admin` > `org_member` > `org_viewer`). A permission set counts when it is assigned to the caller, or to one of the caller's groups, for the whole organization and the assignment has not expired.

One `REJECTED` decision rejects the request. The request becomes `APPROVED` once `min_approvals` different approvers have approved it.

`GET /v1/approvals/requests/{request_id}/decisions` lists the decisions on a request and needs `iam:approval:read`.
