Skip to content
Hexel StudioDocs

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

ActionWhat it allows
iam:jit:writeCreate requests. Activate or revoke your own approved request.
iam:jit:approveApprove or reject other people's requests. Activate, revoke or expire any request.
iam:jit:readRead every request of the organization. Without it you see only your own.

Create a request

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

FieldTypeDescription
role_idstringRequired. An existing role available to your organization. Otherwise 400 (role not found).
workspace_idstringOptional workspace the access is for.
environment_idstringOptional environment. Needs workspace_id.
reasonstringWhy the access is needed.
ticket_idstringOptional reference, such as an incident number.
duration_minutesintegerHow 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": "..."}.

New statusWho may set itWhat is recorded
APPROVED, REJECTEDiam:jit:approve, never on your own requestAPPROVED sets approved_by_user_id and approved_at
ACTIVEThe requester with iam:jit:write, or anyone with iam:jit:approveactivated_at, and expires_at = now + duration_minutes
REVOKEDThe requester with iam:jit:write, or anyone with iam:jit:approverevoked_at and revoked_by_user_id
EXPIREDiam:jit:approveStatus 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"}
StatusWhen
400status is not one of the six statuses.
403You lack the permission for this change, or you are approving or rejecting your own request (a requester cannot approve or reject its own request).
404No such request in your organization.
409The 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"}

Read requests

CallPermissionResponse
GET /v1/jitNo 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 requesterThe 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

ActionWhat it allows
iam:approval:readRead workflows, requests and decisions.
iam:approval:writeCreate, change and delete workflows, and create requests.
iam:approval:manageDecide requests.

Workflows

FieldTypeDescription
namestringName of the workflow.
descriptionstringOptional.
workflow_typestringROLE_ASSIGNMENT, JIT_ACCESS, POLICY_CHANGE or SERVICE_USER_CREATE. A label: it does not connect the workflow to that feature.
scope_type, scope_idstringOptional labels for what the workflow covers.
requires_approvalbooleanStored. Default true.
approver_rolestringOptional. org_owner, org_admin, org_member, org_viewer, or the ID of a permission set. Any other value answers 400.
approver_user_idsarray of stringsOptional. When set, only these users may decide.
min_approvalsintegerDistinct approvals needed. Default 1.
is_enabledbooleanStored. 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
  }'
CallPermissionResponse
POST /v1/approvals/workflowsiam:approval:write201 with the workflow
GET /v1/approvals/workflowsiam:approval:readArray of your organization's workflows
GET /v1/approvals/workflows/{workflow_id}iam:approval:readThe workflow, or 404
PATCH /v1/approvals/workflows/{workflow_id}iam:approval:writeThe updated workflow. id, organization_id and timestamps never change; "approver_role": "" clears the role.
DELETE /v1/approvals/workflows/{workflow_id}iam:approval:write204. The workflow's requests and decisions are deleted with it.

Requests

FieldTypeDescription
workflow_idstringRequired. A workflow of your organization, otherwise 404.
request_typestringWhat is being requested, in your own terms.
request_dataobjectDetails of the request. Stored as given.
reasonstringOptional.
expires_atstringOptional 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"
}
CallPermissionResponse
POST /v1/approvals/requestsiam:approval:write201 with the request
GET /v1/approvals/requestsiam:approval:readArray of your organization's requests, newest first. Filter with ?workflow_id= and ?status=.
GET /v1/approvals/requests/{request_id}iam:approval:readThe request, or 404

Decide a request

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

FieldTypeDescription
decisionstringRequired. APPROVED or REJECTED.
commentstringOptional.
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.