Skip to content
Hexel StudioDocs

Service users, API keys and OAuth clients

Create a non-human identity for a script, server or CI job, give it permissions, and manage its API keys and OAuth clients.

On this page

A service user is an identity that belongs to your organization and is used by software, not a person. It has no password and can't sign in to the console. It authenticates with an API key or an OAuth client, and it can do only what the policies attached to it allow.

Use one service user per workload, so that you can limit, audit and revoke each one on its own.

Before you start

You need an access token for your organization in $ACCESS_TOKEN and the organization ID in $ORG_ID. Each operation needs one of these permissions:

OperationAction
Create a service userdirectory:service_account:create
List or read service users, their keys, clients and policiesdirectory:service_account:read
Update, disable or enable a service userdirectory:service_account:update
Delete a service userdirectory:service_account:delete
Create or revoke API keys; create, rotate or revoke OAuth clientsdirectory:service_account:manage_keys
Attach or detach policiesiam:policy:attach

A service user ID from another organization answers 404, the same as one that doesn't exist.

Create a service user

Bash
curl -X POST https://api.hexelstudio.com/v1/service-users \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "X-Organization-Id: $ORG_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "ci-deployer",
    "display_name": "CI deployer",
    "description": "Deploys from the main branch"
  }'
namestringbodyrequired

A name for the service user.

display_namestringbodyrequired

The name shown in the console.

descriptionstringbody

Optional free text.

The response is 201 with the service user:

JSON
{
  "id": "su_0f8e4b2c9a7d4e1f8b3c6d5a4e3f2a1b",
  "name": "ci-deployer",
  "display_name": "CI deployer",
  "description": "Deploys from the main branch",
  "organization_id": "<org-id>",
  "is_active": true,
  "created_by_user_id": "<user-id>",
  "created_at": "2026-10-20T09:30:00Z",
  "updated_at": "2026-10-20T09:30:00Z"
}

The organization is always the one your token is for, and created_by_user_id is always you.

List, read and update

Bash
# List the organization's service users (a JSON array)
curl https://api.hexelstudio.com/v1/service-users \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# Read one
curl https://api.hexelstudio.com/v1/service-users/<service-user-id> \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# Change its display name or description
curl -X PATCH https://api.hexelstudio.com/v1/service-users/<service-user-id> \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"display_name": "CI deployer (prod)"}'

PATCH changes only the fields you send (name, display_name, description, is_active) and returns the updated service user. id, organization_id, created_by_user_id and the timestamps can't be changed.

Give it permissions

A new service user can do nothing. Attach policies to it:

Bash
curl -X POST https://api.hexelstudio.com/v1/service-users/<service-user-id>/policies \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"policy_id": "<policy-id>"}'

The response is 201 with the attachment:

JSON
{
  "id": "<attachment-id>",
  "policy_id": "<policy-id>",
  "target_type": "SERVICE_USER",
  "target_id": "su_0f8e4b2c9a7d4e1f8b3c6d5a4e3f2a1b",
  "organization_id": "<org-id>",
  "attached_by_user_id": "<user-id>",
  "attached_at": "2026-10-20T09:31:00Z"
}

You can give a service user only what you hold yourself. If the policy allows an action that you aren't allowed across the organization, the request fails with 403 and lists the actions in missing_actions:

JSON
{
  "error": "forbidden",
  "reason": "cannot grant actions the caller does not hold",
  "missing_actions": ["iam:policy:write"]
}

List and remove attachments:

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

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

A service user can also get permissions by assuming a role, for a limited time.

API keys

An API key is a single secret string. Send it in the X-API-Key header, or exchange it for an access token. See Authenticate to the API.

Create an API key

curl -X POST https://api.hexelstudio.com/v1/service-users/<service-user-id>/api-keys \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "github-actions",
    "description": "Deploy workflow",
    "expires_at": "2027-01-31T00:00:00Z"
  }'
namestringbodyrequired

A name that tells you where the key is used.

descriptionstringbody

Optional free text.

expires_atstringbodydefault: 90 days from now

When the key stops working, as an RFC 3339 timestamp. It must be in the future and no later than the end of the day (UTC) one year from now. Otherwise the request fails with 400.

The response is 201:

JSON
{
  "api_key": "studio_live_9c1d0e7f...",
  "id": "ak_3a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d",
  "key_prefix": "studio_live_9c1d0e7f",
  "expires_at": "2027-01-31T00:00:00Z"
}

Every new key expires. expires_at is always set in the response, to your value or to the default 90 days.

List API keys

Bash
curl https://api.hexelstudio.com/v1/service-users/<service-user-id>/api-keys \
  -H "Authorization: Bearer $ACCESS_TOKEN"

The list holds the keys that aren't revoked, including expired ones. Each key shows id, name, description, key_prefix, service_user_id, organization_id, expires_at, last_used_at, created_by_user_id and created_at, never the key itself. Use key_prefix to find which key a system holds. last_used_at is approximate.

Some keys created before October 2026 have no expires_at. They work until you revoke them. Replace them with expiring keys.

Revoke an API key

Bash
curl -X DELETE https://api.hexelstudio.com/v1/service-users/<service-user-id>/api-keys/<key-id> \
  -H "Authorization: Bearer $ACCESS_TOKEN"

The response is 204. See What revocation does.

Replace a key

API keys aren't rotated in place. To replace one without downtime:

  1. Create a new key

    Create a key for the same service user and store it where the old one is used.

  2. Deploy

    Roll out the new key to every system that uses the old one.

  3. Revoke the old key

    Check that the old key's last_used_at has stopped advancing, then revoke it.

OAuth clients

An OAuth client is a client_id and client_secret pair for the OAuth 2.0 client credentials grant. Use it with tools and libraries that speak OAuth. See Use OAuth client credentials.

Create an OAuth client

Bash
curl -X POST https://api.hexelstudio.com/v1/service-users/<service-user-id>/oauth-clients \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "billing-sync", "description": "Nightly billing export"}'
namestringbodyrequired

A name for the client.

descriptionstringbody

Optional free text.

expires_atstringbody

Optional RFC 3339 timestamp after which the client can't get tokens. Without it, the client doesn't expire.

The response is 201:

JSON
{
  "client_id": "client_7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b",
  "client_secret": "studio_secret_4f3e2d1c...",
  "name": "billing-sync"
}

List OAuth clients

Bash
curl https://api.hexelstudio.com/v1/service-users/<service-user-id>/oauth-clients \
  -H "Authorization: Bearer $ACCESS_TOKEN"

The list holds the clients that aren't revoked. Each shows client_id, name, description, client_secret_prefix, is_active, expires_at, last_used_at, rotated_at, created_by_user_id and created_at, never the secret.

Rotate a client secret

Bash
curl -X POST https://api.hexelstudio.com/v1/service-users/<service-user-id>/oauth-clients/<client-id>/rotate \
  -H "Authorization: Bearer $ACCESS_TOKEN"
JSON
{
  "client_id": "client_7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b",
  "client_secret": "studio_secret_8a7b6c5d..."
}

The client_id stays the same. The old secret stops working at once for new tokens, so update every system that uses it before you rotate, or rotate during a quiet period. Access tokens issued with the old secret stay valid until they expire (at most 15 minutes). To cut them off too, revoke the client and create a new one.

Revoke an OAuth client

Bash
curl -X DELETE https://api.hexelstudio.com/v1/service-users/<service-user-id>/oauth-clients/<client-id> \
  -H "Authorization: Bearer $ACCESS_TOKEN"

The response is 204. A revoked client can't be restored or rotated. A key or client ID that isn't this service user's answers 404 with {"error": "credential not found"}.

Disable or delete a service user

Disable a service user to stop it without losing its setup:

Bash
curl -X PATCH https://api.hexelstudio.com/v1/service-users/<service-user-id> \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"is_active": false}'

While it's disabled, none of its API keys or OAuth clients can get a token, and its existing tokens are refused. Send {"is_active": true} to enable it again.

Delete it when you no longer need it:

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

The response is 204. Deleting revokes all its API keys and OAuth clients, and removes its policy attachments and group memberships. It can't be undone.

What revocation does

When you revoke an API key or OAuth client, or disable or delete a service user:

  • The credential can't get new tokens. An API key sent in X-API-Key is refused within seconds.
  • Access tokens the credential already issued are refused by Hexel Studio's identity and access APIs. Allow up to 30 seconds.
  • Some Hexel Studio products may accept already-issued tokens until they expire, at most 15 minutes after they were issued.
  • Role sessions the service user started are not ended by revoking one credential. They last until they expire, or until you revoke the role's sessions. Disabling or deleting the service user ends them.

If a key or secret has leaked, revoke it first, then check the audit log for what it did.

Limits

  • Creating, rotating and revoking API keys and OAuth clients counts against a limit of 20 requests a minute for each caller, on top of the general write limit.
  • A token request with one API key or OAuth client is limited to 60 a minute.
  • API keys expire after 90 days by default and after at most one year.

Over a limit you get 429 with a Retry-After header. See Limits.