Skip to content
Hexel StudioDocs

Write and attach policies

Create a policy, validate it, attach it to members, groups, service users and roles, manage its versions, and grant access in a single workspace or environment.

On this page

This guide creates a policy with the API, attaches it, and changes it safely. Read Policies and permissions first for the document language and what Hexel Studio applies today.

All calls use an access token for your organization in Authorization: Bearer $ACCESS_TOKEN. The organization always comes from the token: policies you create belong to it, and attachments grant only in it.

Permissions you need

StepEndpointAction
Validate a documentPOST /v1/authz/validate-policyiam:authz:simulate
Create, change or delete a policyPOST /v1/policies, PATCH and DELETE /v1/policies/<policy-id>iam:policy:write
List and read policiesGET /v1/policies, GET /v1/policies/<policy-id>None: any member of the organization
Create a versionPOST /v1/policies/<policy-id>/versionsiam:policy:write
List versionsGET /v1/policies/<policy-id>/versionsiam:policy:read
Attach and detachPOST /v1/policies/<policy-id>/attachments, DELETE .../attachments/<attachment-id>iam:policy:write
List attachmentsGET /v1/policies/<policy-id>/attachmentsiam:policy:read
Permission sets and assignments/v1/permission-sets/...iam:permission_set:read to read, iam:permission_set:write to change

Owners hold every action. Admins hold all of these through IAM Admin Access. Viewers hold the read actions.

Attaching a policy, changing the document of a policy, making a version the default and assigning a permission set also require that you already hold every action they grant. Otherwise the call fails with 403 and lists missing_actions. See Nobody grants what they don't hold.

Validate a policy

Hexel Studio does not check a document when you create a policy, and a document it can't read grants nothing. Check it first with POST /v1/authz/validate-policy. Send the document as a JSON string in document:

Bash
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\":[\"workspace:workspace:read\"]}]}"}'
JSON
{"valid": false, "errors": ["statement[0]: missing 'Resource'"]}

To validate a file without escaping it by hand, let jq build the body:

Bash
jq -n --rawfile doc policy.json '{document: $doc}' | \
  curl -X POST https://api.hexelstudio.com/v1/authz/validate-policy \
    -H "Authorization: Bearer $ACCESS_TOKEN" \
    -H "Content-Type: application/json" \
    -d @-

A valid document returns {"valid": true, "errors": null}.

The validator checks that the document is JSON, has at least one statement, and that every statement has Effect set to Allow or Deny, a non-empty Action (a string or an array of strings) and a Resource. It does not check that action names exist, so check spelling against the actions reference.

Create a policy

POST /v1/policies creates a policy in your organization. Hexel Studio also stores the document as version 1 and makes it the default version, which is the one that applies.

curl -X POST https://api.hexelstudio.com/v1/policies \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "workspace-readers",
    "display_name": "Workspace readers",
    "description": "Read workspaces and environments",
    "policy_document": {
      "Version": "2026-02-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": ["workspace:workspace:read", "env:env:read"],
          "Resource": ["*"]
        }
      ]
    }
  }'
namestringbody

A short name. Unique in your organization, including among deleted policies.

display_namestringbody

The name shown in the console.

descriptionstringbody

Optional description.

policy_documentobjectbodyrequired

The policy document, as a JSON object. A document sent as a string is stored as a string and grants nothing.

The response is 201 with the policy:

JSON
{
  "id": "pol_6c1d0e2f...",
  "name": "workspace-readers",
  "display_name": "Workspace readers",
  "description": "Read workspaces and environments",
  "organization_id": "org_9a8b7c...",
  "policy_document": {
    "Version": "2026-02-17",
    "Statement": [
      {"Effect": "Allow", "Action": ["workspace:workspace:read", "env:env:read"], "Resource": ["*"]}
    ]
  },
  "is_system": false,
  "created_by_user_id": "<your-user-id>",
  "created_at": "2026-10-20T09:30:00.000000Z",
  "updated_at": "2026-10-20T09:30:00.000000Z"
}

Creating a policy grants nothing until you attach it.

StatusBodyCause
400{"error": "..."}The body is not valid JSON.
403{"error": "forbidden"}You don't hold iam:policy:write.
500{"error": "..."}The name is already used in your organization, or policy_document is missing.

List and read policies

GET /v1/policies returns an array with your organization's policies and the managed policies ("is_system": true), managed first, then by name. GET /v1/policies/<policy-id> returns one policy, or 404 with {"error": "policy not found"}.

Bash
curl https://api.hexelstudio.com/v1/policies \
  -H "Authorization: Bearer $ACCESS_TOKEN"

The policy_document of a policy is the document from when you created it or last changed it with PATCH. If you made another version the default since, list the versions to see the document that applies.

Attach a policy

POST /v1/policies/<policy-id>/attachments attaches a policy, yours or a managed one, to a principal of your organization.

Bash
curl -X POST https://api.hexelstudio.com/v1/policies/<policy-id>/attachments \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"target_type": "GROUP", "target_id": "<group-id>"}'
target_typestringbodyrequired

Who receives the policy. Case-insensitive.

ValueTargettarget_id
USERA memberThe member's user ID (user_id in the members list)
GROUPEvery member of a groupThe group ID, grp_...
SERVICE_USERA service userThe service user ID, su_...
ROLESessions of a roleThe role ID, role_...
target_idstringbodyrequired

The ID of the target. Groups, service users and roles must belong to your organization. Hexel Studio does not check that a USER target is a member, so copy the ID from the members list.

The response is 201 with the attachment:

JSON
{
  "id": "pa_4e5f6a7b...",
  "policy_id": "pol_6c1d0e2f...",
  "target_type": "GROUP",
  "target_id": "grp_1a2b3c...",
  "organization_id": "org_9a8b7c...",
  "attached_by_user_id": "<your-user-id>",
  "attached_at": "2026-10-20T09:31:00.000000Z"
}

The attachment takes effect for the target's existing tokens, usually at once and always within about a minute.

StatusBodyCause
400{"error": "target must be a USER, SERVICE_USER, GROUP or ROLE of the caller's organization"}Unknown target_type, empty target_id, or a group, service user or role that is not in your organization.
403{"error": "forbidden"}You don't hold iam:policy:write.
403{"error": "forbidden", "reason": "cannot grant actions the caller does not hold", "missing_actions": [...]}The policy allows actions you don't hold.
404{"error": "policy not found"}No such policy in your organization or among the managed policies.
500{"error": "..."}The policy is already attached to this target.

For a service user you can also use POST /v1/service-users/<service-user-id>/policies with {"policy_id": "<policy-id>"}, which needs iam:policy:attach. See Service users.

List attachments

GET /v1/policies/<policy-id>/attachments returns the policy's attachments in your organization, as an array of attachment objects. For a managed policy this shows where your organization uses it, including the membership groups.

Bash
curl https://api.hexelstudio.com/v1/policies/<policy-id>/attachments \
  -H "Authorization: Bearer $ACCESS_TOKEN"

There is no endpoint that lists every policy attached to one member or group. List the attachments of each policy, or check the result with /v1/authz/simulate. For a service user use GET /v1/service-users/<service-user-id>/policies, and for a role GET /v1/roles/<role-id>/policies.

Detach a policy

DELETE /v1/policies/<policy-id>/attachments/<attachment-id> removes an attachment. Take the attachment ID from the list above.

Bash
curl -X DELETE https://api.hexelstudio.com/v1/policies/<policy-id>/attachments/<attachment-id> \
  -H "Authorization: Bearer $ACCESS_TOKEN"

The response is 204. An attachment that is not in your organization, or not of this policy, returns 404 with {"error": "policy attachment not found"}. Detaching needs only iam:policy:write: you can remove access you don't hold yourself.

Update a policy

PATCH /v1/policies/<policy-id> changes your organization's policies. Managed policies return 404.

Bash
curl -X PATCH https://api.hexelstudio.com/v1/policies/<policy-id> \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Read workspaces, environments and service users",
    "policy_document": {
      "Version": "2026-02-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": ["workspace:workspace:read", "env:env:read", "directory:service_account:read"],
          "Resource": ["*"]
        }
      ]
    }
  }'

Send only the fields you change: name, display_name, description or policy_document (an object, or the document as a JSON string). The response is 200 with the policy.

A new policy_document replaces the document of the default version in place, without creating a version, and applies at once to every principal the policy is attached to. Because of that, you must hold every action the new document allows. A document Hexel Studio can't parse returns 400 with {"error": "policy document is not valid"}.

To keep the previous document so you can go back to it, create a version instead.

Manage versions

A policy can have several versions. Exactly one is the default, and only the default applies.

Create a version

Bash
curl -X POST https://api.hexelstudio.com/v1/policies/<policy-id>/versions \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "version_number": 2,
    "is_default": true,
    "policy_document": {
      "Version": "2026-02-17",
      "Statement": [
        {"Effect": "Allow", "Action": ["workspace:workspace:read"], "Resource": ["*"]}
      ]
    }
  }'
version_numberintegerbodyrequired

The version number. Hexel Studio does not assign it: choose a number this policy doesn't have yet. The first version is 1.

policy_documentobjectbodyrequired

The document of this version, as a JSON object.

is_defaultbooleanbodydefault: false

true makes this version the default: it replaces the previous default at once. You must hold every action it allows. false only stores the version.

The response is 201:

JSON
{
  "id": "pv_0f1e2d3c...",
  "policy_id": "pol_6c1d0e2f...",
  "version_number": 2,
  "policy_document": {
    "Version": "2026-02-17",
    "Statement": [{"Effect": "Allow", "Action": ["workspace:workspace:read"], "Resource": ["*"]}]
  },
  "is_default": true,
  "created_by_user_id": "<your-user-id>",
  "created_at": "2026-10-20T09:40:00.000000Z"
}

There is no call to make an existing version the default. To go back to an earlier document, create a new version with that document and "is_default": true.

List versions

GET /v1/policies/<policy-id>/versions returns every version, highest version_number first. It works for managed policies too.

Bash
curl https://api.hexelstudio.com/v1/policies/<policy-id>/versions \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Delete a policy

DELETE /v1/policies/<policy-id> deletes one of your organization's policies. It stops granting everywhere it is attached, at once. The response is 204. Its name stays reserved in your organization.

Bash
curl -X DELETE https://api.hexelstudio.com/v1/policies/<policy-id> \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Grant access in one workspace or environment

A policy attachment always applies to the whole organization. To grant a policy in one workspace or environment only, put it in a permission set and assign the set with a scope.

  1. Create a permission set
    Bash
    curl -X POST https://api.hexelstudio.com/v1/permission-sets \
      -H "Authorization: Bearer $ACCESS_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "staging-deployers",
        "display_name": "Staging deployers",
        "description": "Full workspace and environment access",
        "policy_ids": ["pol_ws_full", "pol_env_full"]
      }'

    policy_ids can name your policies and managed policies. The response is 201 with the permission set, whose ID starts with psd_.

  2. Assign it with a scope
    Bash
    curl -X POST https://api.hexelstudio.com/v1/permission-sets/<permission-set-id>/assignments \
      -H "Authorization: Bearer $ACCESS_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "principal_type": "USER",
        "principal_id": "<user-id>",
        "workspace_id": "<workspace-id>",
        "expires_at": "2026-12-31T23:59:59Z"
      }'
    principal_typestringbodyrequired

    USER, SERVICE_USER or GROUP.

    principal_idstringbodyrequired

    The user ID, service user ID or group ID.

    workspace_idstringbody

    Grant only in this workspace and its environments. Omit both workspace_id and environment_id to grant organization-wide.

    environment_idstringbody

    Grant only in this environment. Set workspace_id to the environment's workspace as well: an assignment with an environment and no workspace grants nothing on API calls.

    expires_atstringbody

    Optional end of the grant, as an RFC 3339 timestamp. Without it the assignment lasts until you remove it.

    The response is 201:

    JSON
    {
      "id": "psa_5d6e7f80...",
      "permission_set_id": "psd_1b2c3d...",
      "principal_type": "USER",
      "principal_id": "<user-id>",
      "organization_id": "org_9a8b7c...",
      "workspace_id": "ws_2c3d4e...",
      "role_id": "role_ps_5d6e7f80...",
      "assigned_by_user_id": "<your-user-id>",
      "assigned_at": "2026-10-20T09:50:00.000000Z",
      "expires_at": "2026-12-31T23:59:59Z"
    }

    Hexel Studio creates a role for each assignment (role_id) and deletes it when you remove the assignment. Don't change it or attach policies to it.

The principal now has the set's policies in that workspace, for requests that name it in X-Workspace-Id (see scope). Hexel Studio does not check that the workspace belongs to your organization: a workspace of another organization never matches a request, so such an assignment grants nothing.

StatusBodyCause
400{"error": "principal_type must be USER, SERVICE_USER or GROUP"}Wrong principal_type.
400{"error": "principal_id is required"}principal_id is missing.
403{"error": "forbidden", "reason": "cannot grant actions the caller does not hold", "missing_actions": [...]}The set's policies allow actions you don't hold.
404{"error": "not found"}No such permission set in your organization or among the managed sets.
404{"error": "policy not found"}The set names a policy that doesn't exist or isn't visible to your organization.

To manage assignments:

  • GET /v1/permission-sets/<permission-set-id>/assignments lists the set's assignments in your organization.
  • DELETE /v1/permission-sets/<permission-set-id>/assignments/<assignment-id> removes one and its access. The response is 204.
  • PATCH /v1/permission-sets/<permission-set-id> changes name, display_name or description.
  • An assignment keeps the policies the set had when you assigned it. To change them, assign a set with the policies you want and remove the old assignment.