Test access
Ask Hexel Studio whether a member or service user may perform an action, see each step of the decision, check several actions at once, and validate policy documents.
On this page
The /v1/authz endpoints answer "may this principal do this?" without making the call. Use them to check a policy before you attach it widely, or to find out why a call was denied.
| Endpoint | Answers |
|---|---|
POST /v1/authz/evaluate | Allowed or denied for one action, with the reason |
POST /v1/authz/explain | The same decision, step by step |
POST /v1/authz/simulate | Allowed or denied for a list of actions |
GET /v1/authz/expand | Some of the principals that have access to a resource |
POST /v1/authz/validate-policy | Whether a policy document is well formed |
Every endpoint needs iam:authz:simulate. Owners and admins hold it by default.
How these results differ from real calls
These endpoints use the full evaluator. Your normal API calls use the cached permission check described in Policies. The two agree on what is allowed by policies, groups and permission sets, but the full evaluator also applies features that don't affect real calls yet:
| The full evaluator applies | Effect on these results | Effect on real calls |
|---|---|---|
Deny statements in policies | Can deny | None |
| Service control policies, resource control policies, permission boundaries on members and service users | Can deny | None. See Guardrails (Preview). |
Session policies, when you send session_id | Can deny | None. See Session policies (Preview). |
ABAC rules (evaluate and simulate only) | Can allow or deny | None. See Fine-grained access (Preview). |
So a result of policy_denied, scp_denied, rcp_denied, boundary_exceeded, session_policy_denied or abac_denied describes what Hexel Studio will enforce later, not what happens today: the real call may succeed.
There are other differences to keep in mind:
- Workspace and environment. These endpoints don't check that
workspace_idandenvironment_idexist in your organization. A real call that names a workspace or environment that isn't active in your organization is denied. - Actions checked at organization level. Some actions, such as
workspace:workspace:create, are always checked at organization level by the real API. Test them withoutworkspace_id. - Roles. You can test members and service users, not roles. A role ID as
subject_idreturnsuser_suspended. - Caching.
evaluateandsimulateresults can be up to 30 seconds old.explainis never cached.
When the subject has no Deny statements and none of the Preview features apply, and you name the same scope as the real call, the answer matches what the real call gets.
Evaluate one action
curl -X POST https://api.hexelstudio.com/v1/authz/evaluate \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subject_id": "<user-id>",
"organization_id": "'"$ORG_ID"'",
"action": "env:env:create",
"workspace_id": "'"$WORKSPACE_ID"'"
}'const res = await fetch("https://api.hexelstudio.com/v1/authz/evaluate", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ACCESS_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
subject_id: "<user-id>",
organization_id: process.env.ORG_ID,
action: "env:env:create",
workspace_id: process.env.WORKSPACE_ID,
}),
});
if (!res.ok) {
throw new Error(`Evaluate failed: ${res.status} ${await res.text()}`);
}
const { allowed, reason } = await res.json();
console.log(allowed, reason);import os
import requests
resp = requests.post(
"https://api.hexelstudio.com/v1/authz/evaluate",
headers={"Authorization": f"Bearer {os.environ['ACCESS_TOKEN']}"},
json={
"subject_id": "<user-id>",
"organization_id": os.environ["ORG_ID"],
"action": "env:env:create",
"workspace_id": os.environ["WORKSPACE_ID"],
},
timeout=10,
)
resp.raise_for_status()
result = resp.json()
print(result["allowed"], result["reason"])The user ID of a member of your organization, or the ID of one of its service users (su_...). Leave it out to test yourself. A person or service user outside your organization returns 404.
Optional. The test always runs in your token's organization; if you send this field, it must be that organization's ID (403 otherwise).
The action to test, for example workspace:workspace:delete. See the actions reference.
The workspace the call would be made in. Grants placed on this workspace count.
The environment the call would be made in. Send workspace_id with it.
A resource type. Only resource control policies (Preview) read it.
A resource ID. Only resource permissions (Preview) read it.
An access token's session ID (sid claim), to apply that session's session policies (Preview).
Request context for ABAC rules (Preview).
Request context for ABAC rules (Preview).
{
"allowed": true,
"reason": "policy_allow",
"cacheable": true,
"principal": "<user-id>",
"action": "env:env:create",
"resource_type": "",
"workspace_id": "ws_2c3d4e...",
"environment_id": "",
"evaluation_ms": 14
}The decision.
Why. See Reasons.
Whether Hexel Studio may reuse this result for the same request for up to 30 seconds.
The subject that was evaluated.
Time taken, in milliseconds. 0 when the result came from the cache.
action, resource_type, resource_id, workspace_id and environment_id echo the request.
Reasons
The steps run in this order, and the first one that decides ends the evaluation:
| Order | Reason | allowed | Meaning |
|---|---|---|---|
| 1 | user_suspended | false | The subject is not an active account or service user. Also returned for unknown IDs and roles. |
| 2 | org_suspended | false | The organization is not active, or the member's membership is not active. |
| 3 | scp_denied | false | A service control policy denies the action (Preview). |
| 4 | rcp_denied | false | A resource control policy for resource_type denies the action (Preview). |
| 5 | boundary_exceeded | false | The subject's permission boundary doesn't allow the action (Preview). |
| 6 | session_policy_denied | false | The session's session policies don't allow the action (Preview). session_policy_unavailable means they couldn't be read. |
| 7 | policy_denied | false | A Deny statement in a policy that applies to the subject matches. Not enforced on real calls yet. |
| 9 | policy_allow | true | An Allow statement matches. This is what real calls check. |
| 10 | resource_permission | true | A resource permission on resource_id allows it (Preview). |
| 11 | abac_allow, abac_denied | true, false | An ABAC rule decides (Preview). |
| 12 | default_deny | false | Nothing allows the action. A real call is denied too. |
Explain a decision
POST /v1/authz/explain takes the same body as evaluate and returns every step it ran. Use it to see which step decided.
curl -X POST https://api.hexelstudio.com/v1/authz/explain \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subject_id": "<user-id>",
"organization_id": "'"$ORG_ID"'",
"action": "iam:policy:write"
}'{
"allowed": false,
"reason": "default_deny",
"steps": [
{"step": "principal", "result": "OK", "details": "uid=<user-id> org=org_9a8b7c..."},
{"step": "user_status", "result": "OK", "details": "ACTIVE"},
{"step": "org_status", "result": "OK", "details": "ACTIVE"},
{"step": "scp_check", "result": "OK", "details": "no SCP blocks this action"},
{"step": "rcp_check", "result": "SKIP", "details": "no resource_type provided"},
{"step": "boundary_check", "result": "SKIP", "details": "no boundary attached"},
{"step": "session_policy", "result": "SKIP", "details": "no session"},
{"step": "explicit_deny", "result": "OK", "details": "no explicit deny"},
{"step": "policy_allow", "result": "NO_MATCH", "details": "no matching Allow policy"},
{"step": "default", "result": "DENY", "details": "no policy grants access"}
]
}Each step has a step name, a result (OK, SKIP, ALLOW, DENY, NO_MATCH or FAIL) and details. The reason values are the ones in Reasons. Other step names can appear; they don't change the decision for your organization.
explain does not run ABAC rules, so it can return default_deny where evaluate returns abac_allow.
Simulate several actions
POST /v1/authz/simulate evaluates a list of actions for one subject in one scope. Each action is evaluated as evaluate would.
curl -X POST https://api.hexelstudio.com/v1/authz/simulate \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"principal_id": "su_4f5a6b...",
"organization_id": "'"$ORG_ID"'",
"actions": ["workspace:workspace:read", "env:env:create", "iam:policy:write"]
}'The user ID of a member of your organization, or the ID of one of its service users. Leave it out to test yourself. Note the name: principal_id, not subject_id.
Optional. If you send it, it must be your token's organization (403 otherwise).
The actions to test.
As for evaluate.
As for evaluate.
As for evaluate.
As for evaluate.
As for evaluate.
{
"results": [
{"action": "workspace:workspace:read", "allowed": true, "reason": "policy_allow"},
{"action": "env:env:create", "allowed": true, "reason": "policy_allow"},
{"action": "iam:policy:write", "allowed": false, "reason": "default_deny"}
]
}List principals with direct grants
GET /v1/authz/expand returns IDs of principals in your organization that have access through two specific kinds of grant:
- users with a permission set assigned directly to them, whatever the set and its scope, and
- principals with a resource permission on the given resource (Preview).
curl "https://api.hexelstudio.com/v1/authz/expand?resource_type=workspace&resource_id=$WORKSPACE_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN"The resource type. Missing: 400 with {"error": "resource_type required"}.
The resource ID.
{"principals": ["<user-id>"]}Validate a policy document
POST /v1/authz/validate-policy checks that a policy document is well formed. Send the document as a JSON string in document:
curl -X POST https://api.hexelstudio.com/v1/authz/validate-policy \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"document": "{\"Version\":\"2026-02-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"env:env:read\",\"Resource\":\"*\"}]}"}'{"valid": true, "errors": null}When the document is not valid, valid is false and errors lists every problem, for example "statement[0]: Effect must be 'Allow' or 'Deny'". Sending document as a JSON object instead of a string returns 400. See Validate a policy for what is checked.
Errors
| Status | Body | Cause |
|---|---|---|
400 | {"error": "..."} | The body is not valid JSON or a field has the wrong type. |
400 | {"error": "sid is not supported; name the principal with subject_id"} | The body has a sid field. Use subject_id, and session_id for session policies. |
400 | {"error": "subject_id is required"} | You are calling with a role session and sent no subject. |
403 | {"error": "forbidden"} | You don't hold iam:authz:simulate. |
403 | {"error": "organization_id must be the token's organization"} | organization_id names another organization. |
404 | {"error": "subject not found in this organization"} | The subject is not a member or service user of your organization. |
503 | {"error": "membership lookup failed"} | Membership couldn't be checked. Try again. |