---
title: "Service tokens"
description: "Manage Alpacon service tokens for automation. Create and approve tokens, rotate keys, restrict access, stop use immediately, and inspect changes and errors."
---

> Full documentation index: https://docs.alpacax.com/llms.txt

# Service tokens

**Endpoint**: `/api/auth/service-tokens/`

Issue a service token for an [application](/reference/api/applications/) to run automation within that application's roles and the token's access-control rules. Its key starts with `alpst-`. A service token cannot call the [users API](/reference/api/iam/users/) or the applications API.

## Authentication and permissions

Token management and history require a person signed in to the console or CLI. Create also accepts the caller's own personal API token with `service_token:create`. Other management actions refuse personal API tokens; all management actions refuse service tokens with `403 permission_denied`.

The examples with `Authorization: Bearer <access_token>` use a signed-in session. These routes require that session except create, which also accepts your own personal API token with `service_token:create` using the token authorization format. No service token can call them. Follow the [setup guide](/reference/api/#automate-with-a-service-token) to create and approve a token. A service token created from the CLI always waits for approval in the web console. See [API authentication](/reference/api/#authentication).

| Action | Required permission |
|--------|---------------------|
| List, get and history | `service_token:read` globally or on the application |
| Create | `service_token:create` globally or on the application |
| Update | `service_token:update`, plus permission to create tokens for that application |
| Rotate | `service_token:rotate`, plus permission to create tokens for that application |
| Disable | `service_token:disable`, plus permission to create tokens for that application |
| Delete | `service_token:delete` |
| Activate or deactivate break-glass | Superuser account, signed in |

An admin can read, create, update, rotate, disable and delete tokens. The application's creator receives the `application:maintainer` role for it: they can read, create and update its tokens, but need an additional grant to rotate, disable or delete them. The `service_token:manager` role also grants read, create and update, globally or for an application. Break-glass is Superuser-only, even for an admin.

Writes are refused with `403 workspace_suspended` while the workspace is suspended, except for activating or deactivating break-glass.

## List tokens

### Request

```http
GET /api/auth/service-tokens/tokens/
```

Only tokens for applications on which you have `service_token:read` appear.

### Query parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `name` | string | Filter by name |
| `enabled` | boolean | Filter by enabled state; `false` includes pending tokens |
| `activation_state` | string | `pending` or `active` |
| `application` | UUID | Filter by application |
| `expiring` | boolean | Live tokens expiring within 14 days |
| `stale` | boolean | Live tokens whose key was issued more than 180 days ago |
| `search` | string | Search name and description |
| `ordering` | string | `added_at`, `updated_at`, `last_used_at`, `expires_at` or `last_issued_at`; prefix with `-` for descending order. Default: `-updated_at` |
| `page` | integer | Page number, starting at 1 |
| `page_size` | integer | Results per page; default 15, maximum 100 |

Use `activation_state=active&enabled=false` to find switched-off tokens, and `activation_state=pending` for tokens awaiting approval.

### Response

`200 OK`, using [integer pagination](/reference/api/#pagination):

```json
{
  "count": 1,
  "current": 1,
  "next": null,
  "previous": null,
  "last": 1,
  "results": [
    {
      "id": "3f1c2a9e-7b6d-4e1f-9a2b-5c8d7e6f4a31",
      "name": "ci-deploy",
      "activation_state": "active",
      "enabled": true,
      "scopes": ["command:create", "command:read"],
      "expires_at": "2027-01-01T00:00:00Z"
    }
  ]
}
```

The example shows selected token fields. The next section lists the full set.

### Example

```bash
curl "https://your-workspace.us1.alpacon.io/api/auth/service-tokens/tokens/?activation_state=pending" \
  -H "Authorization: Bearer <access_token>"
```

## Token fields

| Field | Type | Description |
|-------|------|-------------|
| `id` | UUID | Read-only token ID |
| `name` | string | Required on create; at most 128 characters. Unique among enabled and pending tokens; a disabled token's name is available |
| `description` | string | Optional; default `""` |
| `application` | UUID / object | Required UUID on create; read-only on update. Responses contain `{id, name, service_type, username}` |
| `enabled` | boolean | Read-only on create; changing it requires approval |
| `activation_state` | string | Read-only: `pending` or `active` |
| `scopes` | array[string] | Each scope has at most 64 characters and names a catalog scope or wildcard. Supply scopes or presets on create |
| `presets` | array[string] | Create-only, write-only preset keys; default `[]` |
| `expires_at` | datetime | Required future ISO 8601 time on create; changing it requires approval |
| `rotation_policy` | string | `30d`, `90d`, `180d` or `365d`; default `90d`. Sets the new lifetime when you rotate; does not rotate automatically |
| `ip_whitelist` | array[string] | IPv4/IPv6 addresses or CIDRs, each at most 64 characters; `[]` means unrestricted |
| `rate_limit` | string | `""` uses the fixed default of `1000/hour`. An override is `N/period`, with a positive integer `N` and `second`, `minute`, `hour`, `day`, `s`, `m`, `h` or `d`; at most 32 characters |
| `allowed_time_start`, `allowed_time_end` | time / null | UTC `HH:MM[:SS]`; set both or neither. Null means unrestricted. A start after the end crosses midnight; an end on a whole minute includes that minute |
| `allowed_days` | array[string] | `mon`, `tue`, `wed`, `thu`, `fri`, `sat`, `sun`, case-insensitive; `[]` permits every day |
| `justification` | string | Write-only, at most 1,024 characters; required for medium/high-risk creation and sensitive changes to tokens with medium/high-risk resulting scopes. Whitespace alone is empty |
| `last_used_at`, `usage_count` | datetime / integer | Read-only usage; null unless you are an admin or can create tokens for the application |
| `last_issued_at` | datetime | Read-only; creation or latest rotation time |
| `break_glass_active`, `break_glass_expires_at` | boolean / datetime | Read-only break-glass state |
| `added_at`, `updated_at` | datetime | Read-only timestamps |
| `permissions` | object | Read-only `{update, delete, rotate, disable}` permission hints |
| `key` | string | Returned only on create, including pending creation, and rotation |
| `scope_usage` | array / null | Detail response only; null unless you are an admin or can create tokens for the application |

## Create a token

### Request

```http
POST /api/auth/service-tokens/tokens/
```

The scopes must fit the application's global roles, including when a Superuser creates the token. Presets are merged with explicit scopes; unavailable scopes are removed from presets. The resulting set cannot be empty. Explicitly requesting an unmintable scope returns `400 service_token_scope_not_mintable`.

Service tokens cannot receive `ftp_session:create`, `session:create`, `session:invite`, `session:share`, `tunnel_session:create`, `tunnel_session:reconnect`, `userchannel:create` or `userchannel:update`. Cluster scopes are also refused when the workspace does not offer Kubernetes access.

### Request body

```json
{
  "name": "ci-deploy",
  "application": "9d2e4b71-0c3a-4f5e-8b6d-1a2c3e4f5a6b",
  "scopes": ["command:create", "command:read"],
  "expires_at": "2027-01-01T00:00:00Z",
  "rotation_policy": "90d",
  "justification": "Run deployment checks from CI"
}
```

### Response

The token and its key are created in both cases:

| Status | When | Token state |
|--------|------|-------------|
| `201 Created` | A signed-in admin creates a low-risk token from the web console | `activation_state: "active"`, `enabled: true` |
| `202 Accepted` | Every other creation, including CLI requests, personal API tokens, non-admin callers and medium/high-risk scopes | `activation_state: "pending"`, `enabled: false` |

Any wildcard is high risk. Command, upload, download and session scopes with an access-control requirement are medium risk. Other scopes are low risk. A token with command scopes always returns `202` and requires `justification`, including when a signed-in admin creates it in the web console.

A `202` returns the full token, including `key`, and adds `status`, `risk_level`, `risk_details` and `approval_request_id`. Selected fields:

```json
{
  "id": "3f1c2a9e-7b6d-4e1f-9a2b-5c8d7e6f4a31",
  "key": "alpst-xxxxxxxxxxxxxxxxxx",
  "activation_state": "pending",
  "enabled": false,
  "status": "pending_approval",
  "risk_level": "medium",
  "risk_details": [],
  "approval_request_id": "5cf4106b-b6c5-4ae3-b33c-e90e7a872155"
}
```

`risk_details` contains entries shaped as `{scope, reason, level}`; the array above omits those entries for brevity. Save the key from this response: the approver does not receive it. A pending token authenticates nowhere until approval. The approval request expires after 7 days.

A Superuser or an approver assigned to the request approves it in the web console; CLI and token calls cannot approve. The requester may approve their own request unless an approval-policy rule forbids it. Approval rechecks the application's roles and the expiry time. Rejection, cancellation or expiry invalidates the pending key.

After approval, `GET /api/auth/service-tokens/tokens/{id}/` returns `200` with `activation_state: "active"` and `enabled: true`. Only then can you add [ACLs](/reference/api/acl/): ACL writes require an enabled, approved service token.

### Example

```bash
curl -X POST "https://your-workspace.us1.alpacon.io/api/auth/service-tokens/tokens/" \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{"name":"ci-deploy","application":"9d2e4b71-0c3a-4f5e-8b6d-1a2c3e4f5a6b","scopes":["command:create","command:read"],"expires_at":"2027-01-01T00:00:00Z","justification":"Run deployment checks from CI"}'
```

## Get a token

```http
GET /api/auth/service-tokens/tokens/{id}/
```

`id` is the token UUID. Returns `200` with the token fields and `scope_usage`, but no key. Read `activation_state` and `enabled` to check whether a pending token has been activated.

## Update a token

### Request

```http
PATCH /api/auth/service-tokens/tokens/{id}/
PUT /api/auth/service-tokens/tokens/{id}/
```

`name` and `description` changes apply immediately. A request touching any of these sensitive fields returns `202` and waits for approval, for every caller:

- `scopes`, `enabled`, `expires_at`, `rotation_policy`
- `ip_whitelist`, `rate_limit`
- `allowed_time_start`, `allowed_time_end`, `allowed_days`

This includes `PATCH {"enabled": false}`. Use [Disable a token](#disable-a-token) to stop use immediately. There is no enable route: request `PATCH {"enabled": true}` and wait for approval. The application's roles are checked again when re-enabling a token.

Only one modification may be pending per token. A token still awaiting its initial approval cannot be updated. `application` is read-only: create a new token to use a different application. `presets` are refused on update.

`PUT` also supplies defaults for `ip_whitelist`, `rate_limit` and `allowed_days`, so it returns `202`. Prefer `PATCH` when changing selected fields.

### Response

An immediate update returns `200` with the token and no key. A sensitive update returns:

```json
{
  "status": "pending_approval",
  "risk_level": "medium",
  "risk_details": [],
  "approval_request_id": "5cf4106b-b6c5-4ae3-b33c-e90e7a872155"
}
```

This body contains no key. `risk_details` uses `{scope, reason, level}` entries, omitted above for brevity. Scope changes must fit the application's roles both when requested and when approved.

### Example

```bash
curl -X PATCH "https://your-workspace.us1.alpacon.io/api/auth/service-tokens/tokens/3f1c2a9e-7b6d-4e1f-9a2b-5c8d7e6f4a31/" \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{"rate_limit":"2000/hour","justification":"Support deployment checks across more servers"}'
```

## Rotate a key

```http
POST /api/auth/service-tokens/tokens/{id}/rotate/
```

Send an optional `reason` string of at most 1,024 characters. Rotation needs no approval, but your own global roles must cover the token's stored scopes. Pending and disabled tokens cannot be rotated.

Returns `200` with the token and its new `key`. The old key stops authenticating immediately, with no overlap. Expiry resets to now plus `rotation_policy`, not the old expiry plus that duration; this can make the lifetime shorter. Update the secret in the automation that uses it in the same step.

```bash
curl -X POST "https://your-workspace.us1.alpacon.io/api/auth/service-tokens/tokens/3f1c2a9e-7b6d-4e1f-9a2b-5c8d7e6f4a31/rotate/" \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{"reason":"Scheduled key rotation"}'
```

## Disable a token

```http
POST /api/auth/service-tokens/tokens/{id}/disable/
```

Send an optional `reason` string of at most 1,024 characters. Returns `204 No Content` and stops the token immediately without approval. It also ends all active break-glass access for the token. Pending tokens cannot be disabled.

A token is also disabled if an application role or permission is removed and its stored scopes exceed the application's remaining permissions.

## Delete a token

```http
DELETE /api/auth/service-tokens/tokens/{id}/
```

Send an optional `reason` string of at most 1,024 characters. Returns `204 No Content`; the token stops authenticating. Deleting a pending token also cancels its issuance request and invalidates its key. Its history remains available while the application exists.

## Break-glass

Break-glass temporarily lifts only the IP whitelist, allowed hours and allowed days. The token must still be approved and enabled, with a valid, unexpired key. It cannot authenticate if its application has been deleted. Rate limits, scopes, application roles and ACLs still apply. See [Access-control lists](/reference/api/acl/).

### Activate

```http
POST /api/auth/service-tokens/tokens/{id}/break-glass/
```

Only a signed-in Superuser can activate break-glass, including while the workspace is suspended.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `justification` | string | Yes | Nonblank reason, at most 2,048 characters |
| `duration_hours` | integer | No | 1–8 hours; default 4 |

```json
{
  "justification": "Restore deployment access during the incident",
  "duration_hours": 2
}
```

Returns `201` with `{id, service_token, activated_by, justification, activated_at, expires_at, deactivated_at, is_active}`. A pending token is refused. Activating break-glass does not make a disabled or expired token usable. The exception remains effective only while the activating user is active and a Superuser; deactivation or demotion ends it.

### Deactivate

```http
DELETE /api/auth/service-tokens/tokens/{id}/break-glass/
```

Only a signed-in Superuser can deactivate it. Returns `204` and ends all active break-glass access for this token. If none is active, returns `400 service_token_break_glass_not_active`. Activation and deactivation are recorded in history and send webhooks.

## Token history

### Request

```http
GET /api/auth/service-tokens/history/?service_token={token_id}
GET /api/auth/service-tokens/history/{history_id}/
```

Both routes require `service_token:read` on the application. The list also requires the `service_token` query parameter: a missing parameter returns `400 service_token_history_scope_required`, and an invalid or inaccessible token ID returns `404 api_not_found`. Detail uses the history entry UUID without requiring that query parameter.

### Query parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `service_token` | UUID | Required token ID for the list |
| `change_type` | string | One change type |
| `change_type__in` | string | Comma-separated change types |
| `changed_by` | UUID | User who made the change |
| `added_at__gte`, `added_at__lte` | datetime | Inclusive timestamp bounds |
| `ordering` | string | `added_at` or `-added_at`; default `-added_at` |
| `page`, `page_size` | integer | List pagination; default 15 results, maximum 100 |

Change types are `created`, `activated`, `updated`, `rotated`, `disabled`, `deleted`, `break_glass_activated` and `break_glass_deactivated`.

### Response

The list returns `200` with `count`, `current`, `next`, `previous`, `last` and `results`. Detail returns one history entry:

```json
{
  "id": "660e8400-e29b-41d4-a716-446655440000",
  "service_token_id": "3f1c2a9e-7b6d-4e1f-9a2b-5c8d7e6f4a31",
  "service_token_name": "ci-deploy",
  "change_type": "updated",
  "changed_by": null,
  "changes": {"description": {"old": "", "new": "Deployment checks"}},
  "reason": "Clarify the token's purpose",
  "added_at": "2026-10-24T10:00:00Z"
}
```

Keys are masked as `***`. History remains readable after token deletion, but not after application deletion.

## Read-only helpers

These routes help you choose scopes, presets and servers before creating a token or its ACLs. They require authentication. The scope and server helpers also require permission to create tokens for the requested application; a personal token reading the server helper needs `server:read`.

| Request | Parameters | `200` response |
|---------|------------|----------------|
| `GET /api/auth/service-tokens/scopes/` | Required `application` UUID | `{resources: [{name, actions, acl, allowed_actions}], wildcards}`. `actions` lists the workspace catalog; `allowed_actions` lists what the application can delegate. `wildcards` includes `*` only if the application holds it |
| `GET /api/auth/service-tokens/presets/` | None | Object keyed by preset ID; each value has localized `name` and `description` (`en`, `ko`) and a `permissions` array. Presets exclude unavailable extensions and scopes service tokens cannot receive |
| `GET /api/auth/service-tokens/servers/` | Required `application` UUID; optional `exclude_service_token` UUID to omit servers the token already reaches, `search` by name, `ordering` (`name` or `platform`, default `name`), `page`, `page_size` | Paginated server objects `{id, name, os, platform, is_connected, is_deleted}` for servers both you and the application can reach. Default 15 results, maximum 100 |

Missing `application` returns `400 service_token_application_required`. An invalid, unknown or deleted application returns `404 api_not_found`; lacking permission to create tokens for it returns `403 service_token_application_forbidden`.

## Error responses

| Status | Error code | Description |
|--------|------------|-------------|
| 400 | `unique`, `service_token_name_exists` | Name conflicts with an enabled or pending token |
| 400 | `service_token_scopes_required` | No scopes remain after preset expansion |
| 400 | `service_token_scope_invalid`, `service_token_invalid_preset` | Invalid scope or unknown/unavailable preset |
| 400 | `service_token_scope_not_mintable` | A requested scope cannot be issued to a service token |
| 400 | `service_token_scope_exceeds_role` | Scopes exceed the application's roles |
| 400 | `service_token_scope_exceeds_rbac` | The person rotating the token does not hold its scopes |
| 400 | `api_token_invalid_expiration` | Expiry is not in the future |
| 400 | `service_token_invalid_ip_whitelist`, `service_token_invalid_rate_limit`, `service_token_invalid_time_window` | Invalid IP range, request rate, hours or days |
| 400 | `service_token_justification_required` | Missing justification for medium/high-risk issuance or a sensitive change |
| 400 | `service_token_pending_activation` | The token is still awaiting its initial approval |
| 400 | `service_token_update_pending` | A modification is already awaiting approval |
| 400 | `service_token_presets_not_allowed_on_update` | Presets supplied on update |
| 400 | `service_token_disabled` | A disabled token cannot be rotated |
| 400 | `service_token_request_superseded` | An issuance request can no longer activate its token |
| 400 | `required`, `blank` | Required field missing or empty, including break-glass justification |
| 400 | `service_token_break_glass_not_active` | No active break-glass access to deactivate |
| 400 | `service_token_history_scope_required`, `service_token_application_required` | Required history or helper parameter missing |
| 401 | `auth_token_missing` | Authentication missing |
| 403 | `permission_denied` | Credential type cannot call this management route |
| 403 | `service_token_application_forbidden` | You cannot create tokens for this application |
| 403 | `service_token_break_glass_unauthorized` | Break-glass caller is not a Superuser |
| 403 | `rbac_permission_required`, `rbac_object_permission_required`, `api_token_scope_missing` | Missing role permission or personal-token scope |
| 403 | `workspace_suspended` | Writes refused while the workspace is suspended |
| 404 | `api_not_found` | Token, history entry or application not found or not visible |

When using the issued `alpst-` key, invalid, disabled, pending, expired or deleted tokens, including tokens on deleted applications, return `401 auth_authentication_failed`. IP and time restrictions return `401 auth_ip_not_allowed` or `auth_access_outside_allowed_time`. Exceeding the token's rate limit returns `429 api_rate_limited` with `Retry-After`; inability to verify a credential returns `503 auth_verification_unavailable`. See [API authentication](/reference/api/#authentication).

Source: https://docs.alpacax.com/reference/api/service-tokens/
