Just-in-time access and approvals
Request, approve and track temporary access to a role, and run multi-approver approval workflows. Records only for now.
On this page
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.
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. |
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:
{
"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": "..."}.
| 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.
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"}'{"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. |
{"error": "status change not allowed", "from": "PENDING", "to": "ACTIVE"}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 |
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.
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.
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"
}'{
"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. |
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"}'{
"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(otherwise409,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 (otherwise403); - the caller holds
approver_role, when the workflow sets it (otherwise403). 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.