---
title: "API reference"
description: "Use the Alpacon REST API to manage access and automate commands. Follow service token setup, execution, polling, and records, with authentication and errors."
---

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

# API reference

Alpacon provides a RESTful API to control and manage the states of servers. The root path for the API is `/api/`.

## API endpoint

Your workspace has a dedicated API endpoint:

```
https://<workspace>.<region>.alpacon.io/api/
```

Where:
- `<workspace>`: Your workspace slug
- `<region>`: Your workspace region (assigned at creation)

**Example**: `https://mycompany.us1.alpacon.io`

Open **Personal settings** to view your complete workspace endpoint URL.

All API examples in this documentation use `https://your-workspace.us1.alpacon.io` as a placeholder. Replace it with your actual workspace endpoint.

## Authentication

Send a token in the `Authorization` header. Both kinds of token use the same format:

```http
Authorization: token="alpst-xxxxxxxxxxxxxxxxxx"
```

| Token | Prefix | Use it for |
|-------|--------|------------|
| Service token | `alpst-` | Automation. Commands and file transfers run through a service token under its access-control rules. Issue one for an [application](/use/iam/applications/service-tokens/). |
| Personal API token | `alpat-` | API access as yourself, to read and manage resources within the token's scopes and your current roles. Create one on [API tokens](/reference/api/tokens/). |

- The keyword is lowercase `token`, then `=` and the key. The quotes are optional: `token=alpst-...` works too. `Authorization: Bearer alpst-...` is not accepted for either kind of token.
- Commands run inside a [work session](/use/work-sessions/) or through a service token.
- Use a [service token](/reference/api/service-tokens/) for automated command execution and file transfers.
- Managing personal API tokens needs a signed-in session, not a token. See [API tokens](/reference/api/tokens/#authentication).

Some management routes require a signed-in session. Routes marked session-only refuse `alpat-` and `alpst-` keys. Follow [Automate with a service token](#automate-with-a-service-token) for setup with the CLI and web console. A service token created from the CLI always waits for approval in the web console. Each API page lists its accepted authentication methods.

When a credential is refused, the response is `401` with one of these codes:

| Code | When |
|------|------|
| `auth_token_missing` | No credential, or a personal API token that doesn't exist, is disabled or expired, or belongs to an inactive user |
| `auth_authentication_failed` | A service token that doesn't exist, is disabled, pending or expired, or belongs to a deleted application |
| `auth_ip_not_allowed` | A service token used from outside its allowed IP ranges |
| `auth_access_outside_allowed_time` | A service token used outside its allowed days and hours |
| `auth_mfa_required` | The action needs a recent MFA check. A `401` body also carries `source`, naming the check |
| `auth_token_mfa_missing` | A sign-in token from the console, the CLI or MCP that didn't complete the MFA your workspace requires at sign-in, or completed it with a method the workspace doesn't allow, for example because MFA was turned on after you signed in. Refreshing the token doesn't help: sign in again and complete MFA, with `alpacon login` in the CLI |
| `auth_client_instance_revoked` | A sign-in token from a sign-in that was revoked. Sign in again |

Approve and reject on both the work-session routes and the approvals API instead return `400 auth_mfa_required` without `source` when fresh MFA confirmation is missing. See [Work sessions](/reference/api/work-sessions/).

If Alpacon can't check the credential at that moment, the response is `503` with `auth_verification_unavailable`. Retry the request; the credential was not refused.

## Global information

### HTTP methods

| Method | Usage |
|--------|-------|
| `GET` | List or retrieve resources |
| `POST` | Create a resource |
| `PUT` | Replace a resource entirely |
| `PATCH` | Update specific fields of a resource |
| `DELETE` | Delete a resource |

For more details, see the [MDN HTTP methods reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods).

### URL structures

We have the following API structures.

- List or create: `/api/<collection>/<resource>/`
- Retrieve, update, or delete: `/api/<collection>/<resource>/<id>/`
- Extra actions: `/api/<collection>/<resource>/<id>/<action>/`

For example, `GET /api/iam/users/a540bf0f-8b37-4f03-8546-dd71c6b03329/` retrieves a user's state.

Please note that all urls end with `/`.

### Pagination

Most list endpoints return one page at a time. The work-session [timeline and records](/reference/api/work-sessions/) use cursor pagination with their own response shapes and limits.

For page-number pagination:

| Parameter | Meaning |
|-----------|---------|
| `page` | The page number, from `1` |
| `page_size` | Results per page. Default `15`, at most `100`; a larger value is treated as `100` |

```json
{
  "count": 85,
  "current": 1,
  "next": 2,
  "previous": null,
  "last": 6,
  "results": []
}
```

`count` is the total number of results, `current` the page you are on and `last` the final page. `next` and `previous` are page numbers, not URLs, and are `null` at either end. To read every result, request `page=next` until `next` is `null`. A page past `last` returns `404` with `api_not_found`.

### Status codes

**Success codes**:

| Code | Meaning | Typical method |
|------|---------|----------------|
| `200 OK` | Request succeeded | `GET`, `PUT`, `PATCH` |
| `201 Created` | Resource created | `POST` |
| `202 Accepted` | Accepted for approval or asynchronous processing; the response describes what is pending. For example, a [token scope change](/reference/api/tokens/#update-api-token) can require approval | `PATCH`, `PUT`, `POST` |
| `204 No Content` | Resource deleted | `DELETE` |

Approval does not always mean `202`. [Work-session creation](/reference/api/work-sessions/) returns `201` with `status: "pending"` when approval is needed; poll the session to learn the outcome.

**Error codes**:

| Code | Meaning |
|------|---------|
| `400 Bad Request` | Invalid request data. See [error messages](#error-messages) for field-level details. |
| `401 Unauthorized` | Not authenticated. See [authentication](#authentication) for the codes. |
| `402 Payment Required` | The workspace's plan, or a workspace extension it hasn't enabled, doesn't cover this action. See [refusals](#refusals). |
| `403 Forbidden` | Permission denied. The credential is valid but may not do this. The body is a coded [refusal](#refusals). |
| `404 Not Found` | The resource doesn't exist, or the credential can't see it. The code is `api_not_found`. |
| `405 Method Not Allowed` | HTTP method not supported on this endpoint. |
| `409 Conflict` | The request is valid, but the resource's current state refuses it. Retrying later may succeed. |
| `429 Too Many Requests` | Rate limited. Carries a `Retry-After` header. See [rate limits](#rate-limits). |
| `503 Service Unavailable` | A dependency couldn't answer: `auth_verification_unavailable` while checking a credential, or `api_search_unavailable` while reading session history. Retry the request. |
| `500 Server Error` | Internal server error. |

For the full list of HTTP status codes, see the [MDN HTTP status reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status_codes).

### Error messages

Every error response carries a machine-readable `code`. The one exception is a `405` for a method the endpoint doesn't support, which carries `{"detail": "..."}`. A `400 Bad Request` additionally carries `field_errors` when Alpacon can pin the problem to a field—or to the request as a whole, under the `non_field_errors` key inside `field_errors`.

For example, when you are changing a user's password, if the new password does not pass server-side validation, you will see a response like this. The field name, `new_password`, is used as the key inside `field_errors`. Build your integration against `code`, not `message`—`message` is often a generic string like "Invalid input." and isn't guaranteed to stay in English.

```
HTTP 400 Bad Request
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
```

```json
{
  "code": "password_too_short",
  "field_errors": {
    "new_password": [
      {
        "code": "password_too_short",
        "message": "Invalid input."
      }
    ]
  }
}
```

`field_errors` maps each field to a list of `{code, message}` entries—a field can carry more than one entry when more than one check on it fails.

An error that isn't about one specific field—for example, an object-level rule that depends on more than one value in the request—is filed under the `non_field_errors` key inside `field_errors` instead of a field name:

```json
{
  "code": "approval_superuser_approve_required",
  "field_errors": {
    "non_field_errors": [
      {
        "code": "approval_superuser_approve_required",
        "message": "Invalid input."
      }
    ]
  }
}
```

Not every `400` carries `field_errors`. A validation failure raised without a specific field—for example, deleting the only remaining owner of a group—carries only the top-level `code`:

```json
{
  "code": "user_unique_group_owner"
}
```

A few codes can come from any endpoint:

| Status | Code | When |
|--------|------|------|
| 400 | `api_invalid_data` | The request body can't be parsed, such as malformed JSON. A few endpoints also use it for data they can't accept |
| 406 | `api_not_acceptable` | The `Accept` header asks for a format the endpoint can't return |
| 415 | `api_unsupported_media_type` | The `Content-Type` isn't one the endpoint accepts. Send the type the endpoint documents, usually `application/json` |
| 500 | `api_unknown_error` | An unexpected server error, usually with `500`. Retry a read. For a request that changes something, such as running a command, check whether it took effect before sending it again |

### Application role validation errors

These Application role grant and removal errors return `400`. See [Let an application manage one alert rule](/use/iam/applications/#let-an-application-manage-one-alert-rule) for the console steps.

| Code | Meaning | What to do |
|------|---------|------------|
| `rbac_application_role_global_required` | The application has no workspace-wide role, so a rule-scoped grant is refused | Give the application a workspace-wide role that covers the required token scopes, then retry |
| `rbac_application_rule_roles_remain` | Rule-scoped roles remain, so the application's last workspace-wide role cannot be removed | Remove the rule-scoped roles first, then remove the workspace-wide role |

### Rate limits

Each service token may make 1,000 requests an hour unless its own rate limit says otherwise; an administrator can set a different limit per token, as a count per second, minute, hour or day. Personal API tokens and signed-in sessions have no general limit, though a few endpoints have their own.

Over the limit, the response is `429` with a `Retry-After` header, the number of seconds to wait:

```
HTTP 429 Too Many Requests
Retry-After: 120
```

```json
{
  "code": "api_rate_limited",
  "gate": "rate"
}
```

Wait for `Retry-After` before retrying. A pipeline that hits the limit often needs a higher limit on its token, not faster retries.

### Refusals

  Available from Alpacon server 2.37.0.

A `402`, `403`, `405`, or `429` response that already carries a `code` may additionally carry fields that explain why the request was refused:

- **`gate`**: the class of control that refused the request.
- **`missing`**: the class of prerequisite that would have let the request through—or, for a token-scope refusal only, the literal scope string the credential lacks.
- **`hint`**: one English statement of at most two sentences, for a person to read. It says what you can do yourself, or what is missing and where the state of an existing request can be read.

`gate` and `missing` are optional and appear only where the server recorded them at the moment of refusal; treat their absence like any other field you don't recognize and ignore it. `hint` is a string of at most 256 bytes with no control characters. It may appear on an authenticated refusal when the server has a matching hint for the code and gate. Print it as it is and never parse it: build your logic on `code`, `gate` and `missing`. Each code's `hint` is quoted in [Refusal codes](#refusal-codes). None of these fields is ever present on a `401`. A `401` carries its `code`; a `401 auth_mfa_required` also carries `source`, naming the MFA check it needs. Approve and reject on both the work-session routes and the approvals API use `400 auth_mfa_required` without `source`.

**`gate` values**:

| Value | Meaning |
|-------|---------|
| `work_session` | The request exceeds what the active work session's scope allows |
| `role` | The caller's RBAC role doesn't grant this permission |
| `token_scope` | The API or service token doesn't hold a scope this action requires |
| `server_reach` | The credential's server access-control rules don't cover this server |
| `command_acl` | The command isn't pre-approved for this credential |
| `file_acl` | The token's file access rules refused this transfer: its path, account and transfer direction |
| `approval` | The action needs a human approval that hasn't been granted |
| `presence` | The action needs a fresh MFA/step-up proof |
| `risk` | Risk-based execution control refused the action |
| `sudo` | The sudo request was refused |
| `plan` | The workspace's plan or an extension doesn't cover this action (`402`)—see [plan-limit refusals](#plan-limit-refusals) below for the additional `axis` field on a resource-limit refusal |
| `rate` | A time-based rate limit refused the request (`429`) |

**`missing` values**:

| Value | Meaning |
|-------|---------|
| `command_preapproval` | A pre-approval for this command would have let it through |
| `file_preapproval` | A file access rule allowing this path, account and transfer direction would have let it through |
| `approval` | An approval would have let it through |
| `presence` | Fresh presence proof would have let it through |
| `work_session` | An active work session would have let it through |
| `plan` | A higher plan tier would have let it through |
| `role` | A role granting this permission would have let it through |
| `approved_account` | Running as the account the work session's approval covered would have let it through |
| `other_approver` | An approver other than the requester would have let it through |
| `allowed_agent` | An agent the workspace's approval policy allows would have let it through |
| `scope:<resource>:<action>` | The one literal form `missing` may take: `scope:` followed by the scope the token lacks, in the platform's own `resource:action` form—e.g. `scope:server:update` names the `server:update` scope. The `scope:` prefix belongs to this refusal response; the scope itself, wherever else you see or request it (on the token, in the scope catalog), is just `server:update`. A single string, or a list of strings when more than one scope is missing. |

**Example—role refusal**:

```json
{
  "code": "rbac_permission_required",
  "gate": "role",
  "missing": "role",
  "hint": "Your roles do not allow this action; it needs a role that covers it."
}
```

**Example—token scope missing**:

```json
{
  "code": "api_token_scope_missing",
  "gate": "token_scope",
  "missing": "scope:server:update"
}
```

`gate`, `missing` and `hint` never carry a rule's contents, another principal's identity, or a path to where the missing thing could be requested. `gate` and `missing` name only the class of control and the class of prerequisite, and `hint` speaks only of what is missing and what you can do within your own access. Treat any field on this envelope you don't recognize as informational and safe to ignore.

### Plan-limit refusals

  Available from Alpacon server 2.39.0.

A `402` with `gate: "plan"` that refuses a resource limit, rather than a feature your plan doesn't include at all, also carries an `axis` naming which limit was hit:

| `axis` value | What it names |
|---|---|
| `server` | Registered servers |
| `user` | Workspace members, including pending invitations |
| `application` | Applications (identities for automation) |
| `websh` | Websh session time in the current usage period |
| `webftp` | WebFTP transfer volume in the current usage period |
| `websh-share` | Time spent sharing Websh sessions with other users in the current usage period |
| `workspace` | Creating another Free workspace while you already belong to one |

`server`, `user`, and `application` are count limits: the body also carries `next`, a relative API path to the workspace's plan and limits. That path answers a signed-in console session and refuses token authentication, so if you're calling with a token, treat `next` as informational and check current usage in the console instead. A count limit has no reset: it lifts once you remove something or the plan changes.

`websh`, `webftp`, and `websh-share` are monthly limits: instead of `next`, the response carries a `Retry-After` header, the number of seconds until the usage period resets. On a plan that doesn't include Websh sharing at all, a `websh-share` refusal has no reset to wait for and carries no `Retry-After`. `workspace` is a rule on your account, the one Free workspace you can belong to, rather than a limit inside a workspace, so it carries neither `next` nor `Retry-After`.

The body never carries your current usage or the limit's numeric value. Read those from the path in `next`, or from the plan and usage details in the console.

**Example—count limit (server)**:

```
HTTP 402 Payment Required
```

```json
{
  "code": "server_limit_exceeded",
  "gate": "plan",
  "axis": "server",
  "next": "/api/workspaces/workspaces/<workspace_id>/entitlements/"
}
```

**Example—monthly limit (websh)**:

```
HTTP 402 Payment Required
Retry-After: 864000
```

`864000` is an illustrative value (10 days); each response carries the seconds left in your own usage period.

```json
{
  "code": "websh_limit_exceeded",
  "gate": "plan",
  "axis": "websh"
}
```

A `402` with `gate: "plan"` and no `axis` is a feature lock: your plan doesn't include the feature at any usage, and no amount of waiting or removing lifts it.

```json
{
  "code": "workspace_enterprise_plan_required",
  "gate": "plan"
}
```

### Refusal codes

At `402`, `403`, `405` and `429`, every code below answers with a [refusal](#refusals) body: plan refusals at `402`, rate refusals at `429`, `workspace_put_info_disabled` and `command_acl_moved` at `405`, and the rest at `403`. Some of these codes also answer at another status, where the body carries no refusal fields and no `hint`: `auth_mfa_required` at `400` and `401`, `api_not_for_alpacon`, `permission_denied` and `user_no_permission` at `400`, and `workspace_unclaimed` and `workspace_not_unclaimed` at `409`. The meaning names the `gate` and `missing` a refusal carries, when it carries them.

Each entry under "What to do" opens with the code's `hint`, word for word. When a code can be refused at more than one `gate`, the entry has one `hint` per `gate`, each after a label such as `server_reach` gate. The rest of the entry says where to make the change. Codes for a refused sudo command are in [PAM and sudo issues](/use/work-sessions/terminal/troubleshoot-sudo/#alpacon-denied-this-sudo-command).

For access decisions and recovery by action, see [Related error codes](/reference/access-rules/#related-error-codes).

#### Roles and permissions

| Code | Meaning | What to do |
| --- | --- | --- |
| `rbac_permission_required` | Your roles don't grant a permission this action needs. `gate: role`, `missing: role`. | Your roles do not allow this action; it needs a role that covers it. |
| `rbac_object_permission_required` | Your roles don't grant this action on this particular item. `gate: role`, `missing: role`. | Your roles do not allow this action on this item; it needs a role that covers it. |
| `rbac_application_update_required` | A request touching several applications includes at least one your roles may not update. | Your roles do not allow updating at least one of these applications; it needs a role that covers each of them. |
| `permission_denied` | The credential may not perform this action. The ACL routes also answer every token with this code. | This account or credential does not have permission for this action. |
| `user_no_permission` | Your account may not perform this action on users. | Your account does not have permission for this action. |
| `user_privileged_removal_forbidden` | The account you are removing holds a platform role, and removing it needs a permission you don't have. | Removing an account that holds a platform role needs a permission your roles do not include. |
| `membership_group_owner_required` | Only the group's top role may change this membership. | Changing this membership needs the top role in the group. |
| `membership_privilege_required` | Your role in the group is too low to add or remove this member. | Your role in this group is not high enough to add or remove this membership. |
| `workspace_ai_corpus_owner_required` | Only the workspace's highest role may change this AI setting. `gate: role`, `missing: role`. | Your role cannot change this setting; it needs the workspace's highest role. |
| `access_rules_admin_only` | Reading the access rules, or running this kind of evaluation, needs a higher role. | Your role cannot read the access rules or run this kind of evaluation; it needs a role that covers it. |
| `metrics_alert_rule_core_admin_only` | The core disk usage rule and its per-server overrides need a higher role to change. | Your role cannot change the core disk rule or its per-server overrides; it needs a role that covers it. |
| `package_delete_not_allowed` | Deleting a package also deletes the files in it, and your roles don't allow deleting every one of them. `gate: role`, `missing: role`. | Deleting this package also deletes its entries, and your roles do not allow deleting them; it needs a role that covers it. |
| `service_token_application_forbidden` | Your roles don't let you issue or change service tokens for this application. | Your roles do not allow issuing or changing service tokens for this application. |
| `service_token_break_glass_unauthorized` | Turning on the break-glass scope for a service token needs the workspace's highest role. | Turning on break-glass access needs the workspace's highest role. |
| `group_agent_approval_policy_authority_required` | Which group needs agent approval is part of the approval policy, and your role may not change it. | Changing which group needs agent approval is an approval policy change, which your role does not allow. |
| `workspace_execution_control_mode_superuser_required` | Only the workspace's highest role may change the execution control mode. | Changing the execution control mode needs the workspace's highest role. |
| `cert_revoke_target_denied` | You are not the certificate's requester and have no manage access to its authority's server. `gate: server_reach`. | Revoking this certificate takes being its requester or holding manage access to its authority's server. |
| `event_command_not_requester` | The command was requested by someone else. | Your account may not act on this command, which it did not request. |
| `approval_request_not_approver` | Your account is not one that may decide this approval request. | Your account may not decide this request. |
| `approval_request_not_requester` | Only the account that raised an approval request may cancel it. | Your account may not cancel this request, which it did not raise. |
| `local_access_claim_not_maintainer` | Claiming a login as an application's automation needs someone who maintains that application. | Only someone who maintains the application this account belongs to may claim this login as its automation. |

#### Tokens and credentials

| Code | Meaning | What to do |
| --- | --- | --- |
| `api_token_scope_missing` | The token lacks a scope the action needs. `gate: token_scope`, `missing` names the scope, prefixed `scope:`. | This token does not hold a scope this action needs; use a token that has the scope, then retry. Add the scope to the token; a scope change can require approval. |
| `api_token_scope_action_unresolved` | No token scope maps to this action, so no token may perform it. `gate: token_scope`. | No token scope could be worked out for this action, so a token may not perform it; perform it from a console sign-in instead. |
| `api_token_acl_not_allowed` | The token's access-control rules don't cover this request: its server rules (`gate: server_reach`), its command rules (`gate: command_acl`, `missing: command_preapproval`) or its file rules (`gate: file_acl`, `missing: file_preapproval`). | `server_reach` gate: This token's server access rules do not cover this server. `command_acl` gate: This token's command access rules do not pre-approve this command. `file_acl` gate: This token's file access rules do not allow this transfer for this path, account and direction. Add a matching rule to the token's server, command or file access-control list. |
| `agent_credential_not_accepted` | A server's agent credential made a request it may not make. `gate: role`, `missing: role`. | A server's agent credential cannot make this request; make it as a person or with an API token instead. |
| `api_not_for_alpacon` | The route does not accept a server's agent credential. | This request is not open to a server's agent credential. |
| `local_access_principal_not_supported` | The action needs a person signed in to the console; API and service tokens are refused. `gate: presence`. | Only a person signed in to the console can do this; an API or service token cannot. |
| `auth_interactive_channel_requires_presence` | A token tried to open a Websh, editor or WebFTP session. | This credential cannot stand for a person at the keyboard, so it cannot open a Websh, editor or WebFTP session; sign in to the CLI or the web console instead. |
| `auth_mfa_requires_human_principal` | A token or an agent tried an action that needs a fresh MFA check. | This action needs a fresh MFA check, which only a person can give; perform it as a signed-in person instead. |
| `cert_principal_not_supported` | A service token or an agent tried to file a certificate request. | A certificate request records the person who filed it, so a service token or an agent cannot file one; file it as a person. |
| `cloud_principal_not_supported` | A service token called a cloud account, cloud instance, or installation job API. Reads are refused too. | Cloud accounts, instances and install jobs are for people, so a service token cannot read or change them; do it as a person. |
| `package_principal_not_supported` | A service token tried to create a package or upload a file into one. | A package and its entries record the person who uploaded them, so a service token cannot create one; do it as a person. |
| `metrics_alert_rule_agent_not_allowed` | An agent credential tried to create an alert rule. | An agent credential cannot create an alert rule; create it as a person instead. |
| `kube_audit_not_a_cluster` | An agent that does not belong to a registered cluster tried to report Kubernetes audit events. | Only a registered cluster's agent may report Kubernetes audit events. |
| `kube_session_policy_not_a_cluster` | An agent that does not belong to a registered cluster tried to read a Kubernetes session's limits. | Only a registered cluster's agent may read a Kubernetes session's limits. |
| `sudo_verify_credential_cannot_prove_mfa` | The credential verifying a sudo request cannot prove an MFA check. `gate: presence`. | This credential cannot prove an MFA check; verify the sudo request from a console or CLI sign-in instead. |

#### MFA checks

| Code | Meaning | What to do |
| --- | --- | --- |
| `auth_mfa_required` | The action needs a recent MFA check. `gate: presence`, `missing: presence`. | This action needs a recent MFA check; complete MFA, then retry. |
| `sudo_policy_bypass_agent_session_needs_presence` | Changing a session-authorized sudo policy on an agent session needs a fresh MFA check. `gate: presence`, `missing: presence`. | Changing a session-authorized sudo policy on an agent session needs a fresh MFA check; complete MFA, then retry. |
| `workspace_execution_control_mode_presence_required` | Changing the execution control mode needs a fresh MFA check. | Changing the execution control mode needs a fresh MFA check; complete MFA, then retry. |
| `workspace_sudo_with_mfa_step_up_unavailable` | The deployment cannot run the sudo MFA check, so the workspace's sudo setting can't be changed. `gate: presence`. | This Alpacon deployment cannot run the sudo MFA check, so sudo through Alpacon cannot be changed here. |
| `workspace_block_local_sudo_step_up_unavailable` | The deployment cannot run the sudo MFA check, so local sudo can't be blocked. `gate: presence`. | This Alpacon deployment cannot run the sudo MFA check, so local sudo cannot be blocked here. |

#### Approvals and work sessions

| Code | Meaning | What to do |
| --- | --- | --- |
| `approval_requester_may_not_approve` | You tried to approve or reject a request you raised yourself. `gate: approval`, `missing: other_approver`. | You cannot decide a request you raised; it needs a decision from someone other than you. |
| `sudo_policy_bypass_agent_session_needs_approval` | Adding a session-authorized sudo policy to an agent session needs a person's approval. `gate: approval`, `missing: approval`. | A session-authorized sudo policy on an agent session needs approval; add it as a change to the work session so that a person can approve it. |
| `work_session_account_not_approved` | An agent session tried to run as an account its approval did not cover. `gate: work_session`, `missing: approved_account`. | This agent session runs only as the account its approval covered; use that account, or request a work session for the account you need. |
| `work_session_agent_not_allowed` | The workspace's approval policy does not allow this agent. `gate: approval`, `missing: allowed_agent`. | The workspace's approval policy does not allow this agent; use an agent the policy allows. |

#### Console-only actions

| Code | Meaning | What to do |
| --- | --- | --- |
| `access_rules_console_only` | Access rules are answered only in the web console. | Access rules are answered only in the web console, not to the CLI, MCP or a token; use the console instead. |
| `ai_agent_attach_console_only` | AI agent credentials can be attached only from the web console. | AI agent credentials can be attached only from the web console; use the console instead. |
| `approval_policy_console_only` | Approval policies are read and changed only in the web console. | Approval policies can be read and changed only from the web console; use the console instead. |
| `group_agent_approval_console_only` | Which group needs agent approval is changed only in the web console. | Which group needs agent approval can be changed only from the web console; use the console instead. |
| `sudo_policy_console_only` | A sudo policy is created or widened only in the web console. | A sudo policy can be created or widened only from the web console; make the change there. |
| `work_session_sudo_bypass_console_only` | A session-authorized sudo policy is added to a work session only in the web console. | A session-authorized sudo policy can be added to a work session only from the web console; add it there. |
| `workspace_execution_control_console_only` | Execution control is loosened only in the web console. | Loosening execution control can be done only from the web console; make the change there. |
| `workspace_security_settings_console_only` | The workspace's MFA policy is weakened only in the web console. | Weakening the workspace's MFA policy can be done only from the web console; make the change there. |
| `workspace_force_delete_portal_only` | A forced workspace deletion is accepted only from the Alpacon portal. | A forced workspace deletion is accepted only from the Alpacon portal. |

#### Methods refused with 405

| Code | Meaning | What to do |
| --- | --- | --- |
| `workspace_put_info_disabled` | A `PUT` to the workspace's details is refused with `405`. | The workspace's details take partial updates only; send the change as a PATCH instead. |
| `command_acl_moved` | Command ACLs on this deployment are managed by Alpacon AI, so the route answers `405`. | Command ACLs are managed by Alpacon AI on this deployment, so they cannot be changed here. |

#### Workspace state

| Code | Meaning | What to do |
| --- | --- | --- |
| `workspace_suspended` | The workspace is suspended. | This workspace is suspended, so changes are refused until its plan is downgraded or renewed. |
| `workspace_unclaimed` | The workspace was set up for its first owner, who has not claimed it yet. | This workspace is not claimed yet, so only the person invited to claim it may join. |
| `workspace_not_unclaimed` | The action applies only to a workspace whose first owner has not claimed it yet, and this one has been claimed. | This action needs a workspace that is not claimed yet. |
| `workspace_extension_not_enabled` | The extension is on the workspace's plan but switched off. | This extension is on the workspace's plan but switched off; it has to be turned on in the workspace's settings first. |

#### Plan limits

These answer `402` with `gate: "plan"`. See [plan-limit refusals](#plan-limit-refusals) for `axis`, `next` and `Retry-After`.

| Code | Meaning | What to do |
| --- | --- | --- |
| `server_limit_exceeded` | The workspace reached its plan's server limit (`axis: server`). `gate: plan`. | The workspace has reached its plan's server limit, so a new server is refused while existing ones keep working; it needs a free slot or a plan with a higher limit. |
| `user_limit_exceeded` | The workspace reached its plan's user limit, pending invitations included (`axis: user`). `gate: plan`. | The workspace has reached its plan's user limit, pending invitations included; it needs a free seat or a plan with a higher limit. |
| `application_limit_exceeded` | The workspace reached its plan's application limit (`axis: application`). `gate: plan`. | The workspace has reached its plan's application limit; it needs a free slot or a plan with a higher limit. |
| `websh_limit_exceeded` | The workspace used its plan's Websh time for this period (`axis: websh`). `gate: plan`. | The workspace's plan allows no more Websh time in this period, so new sessions are refused while running ones continue. Wait for the `Retry-After` seconds, when the period resets. |
| `webftp_limit_exceeded` | The workspace used its plan's WebFTP transfer volume for this period (`axis: webftp`). `gate: plan`. | The workspace's plan allows no more WebFTP transfer volume in this period, so new transfers are refused. Wait for the `Retry-After` seconds, when the period resets. |
| `websh_share_limit_exceeded` | The plan does not include Websh sharing, or its sharing time for this period is used up (`axis: websh-share`). `gate: plan`. | The workspace's plan does not include Websh sharing, or its sharing time for this period is used up. When the response carries `Retry-After`, sharing is available again after that many seconds. |
| `workspace_free_limit_exceeded` | You tried to create a second Free workspace (`axis: workspace`). `gate: plan`. | You already have a Free workspace you signed up for, and an account can hold only one. |
| `workspace_enterprise_plan_required` | The feature needs the Enterprise plan. `gate: plan`. | This needs the Enterprise plan, which the workspace is not on. |
| `workspace_slug_change_requires_enterprise` | Changing the workspace's address needs the Enterprise plan. `gate: plan`. | Changing the workspace's address needs the Enterprise plan. |
| `workspace_extension_plan_required` | The workspace's plan does not include this extension. `gate: plan`. | The workspace's plan does not include this extension; it needs a plan that does. |
| `workspace_plan_update_required` | The workspace's plan does not include this feature. `gate: plan`. | The workspace's plan does not include this; it needs a plan that does. |

#### Rate-limit refusals

These answer `429` with `gate: "rate"`. See [rate limits](#rate-limits) for a service token's request limit.

| Code | Meaning | What to do |
| --- | --- | --- |
| `api_rate_limited` | The caller went over a request limit. `gate: rate`. | Too many requests were made in a short time; wait, then retry. Wait for the `Retry-After` seconds before retrying. |
| `rag_rate_limit_exceeded` | Too many AI assistant requests in a short time. `gate: rate`. | Too many assistant requests were made in a short time; wait, then retry. Wait for the `Retry-After` seconds before retrying. |
| `workspace_invite_rate_limited` | Too many invitations in a short time. `gate: rate`. | Too many invitations were sent in a short time; wait, then send again. |
| `confirm_token_resend_limit_exceeded` | A confirmation was resent as many times as allowed. `gate: rate`. | This confirmation has been resent as many times as allowed; wait, then try again. |

## Automate with a service token

Use alpacon-cli 1.14.2 or later, with Bash, `curl` and `jq` installed. Sign in with `alpacon login` as a Superuser or an admin, selecting the same workspace as `ALPACON_URL` below. The [`alpacon api`](/reference/cli/api/) requests use that signed-in session and ask for MFA when needed. They print the response body to stdout; HTTP errors also print `HTTP <code>` to stderr and exit with code `1`.

Steps 3, 5b, 6 and the self-hosted part of step 7 need a signed-in session. Steps 1, 2 and 4 also accept your own personal API token with the matching scopes, but no service token. Use your signed-in CLI session for all of those requests in this guide. A service token created from the CLI always waits for approval in the web console. Only the web console can approve it in step 5a; step 7 on Cloud also uses the console.

Set your workspace URL and the ID of one connected, commissioned Linux server running a current agent. Fill the remaining variables as you follow the steps. Response examples show only the relevant fields, with illustrative IDs and timestamps.

```bash
alpacon login
ALPACON_URL='https://your-workspace.us1.alpacon.io'
SERVICE_TOKEN=''
APP_ID=''
ROLE_ID=''
TOKEN_ID=''
APPROVAL_REQUEST_ID=''
SERVER_ID=''
COMMAND_ID=''
read -r -p 'Connected server ID: ' SERVER_ID
```

### 1. Create the application

[Create an application](/use/iam/applications/) named `CI pipeline` with service type `ci_cd` before configuring its server account:

```bash
alpacon api -X POST /api/iam/applications/ \
  -f 'name=CI pipeline' -f service_type=ci_cd
```

Expected: `201 Created`.

```json
{"id":"<application-id>","name":"CI pipeline","service_type":"ci_cd","username":null,"shell":"/usr/sbin/nologin","provisioning_enabled":false,"auto_provisioning":false}
```

Save the application's `id`:

```bash
read -r -p 'Application ID: ' APP_ID
```

### 2. Set the application's server account

Set the application's username to `ci-deploy` and enable provisioning. Choose a username that does not match an existing server account or group. The first command creates the account on a Linux server running a current agent:

```bash
alpacon api -X PATCH "/api/iam/applications/$APP_ID/" \
  -f username=ci-deploy -F provisioning_enabled=true
```

Expected: `200 OK`, with the updated application:

```json
{"id":"<application-id>","username":"ci-deploy","provisioning_enabled":true}
```

An existing account or group name returns `400 approval_superuser_approve_required`; complete that approval before continuing. To bind an existing server account instead, see the application's [server account binding API](/reference/api/applications/).

### 3. Grant the application a global role

Grant the application the built-in `operator` role globally, without restricting the grant to a resource. This role includes `command:create` and `command:read`. You must be able to update the application and grant the role's permissions. The token's scopes must fit the application's global role.

**3a. Find the role:**

```bash
alpacon api -X GET /api/rbac/roles/ -f name=operator
```

Expected: `200 OK`, with `results` containing the role:

```json
{"results":[{"id":"<role-id>","name":"operator"}]}
```

Save its `id` as `ROLE_ID`:

```bash
read -r -p 'Operator role ID: ' ROLE_ID
```

**3b. Grant the role:** omit `content_type` and `object_id`. Granting a role requires a fresh MFA check. Complete the prompt opened by `alpacon api`; without a fresh check, the API returns `401 auth_mfa_required` with `source: "rbac_binding"`.

```bash
alpacon api -X POST /api/rbac/application-roles/ \
  -f "application=$APP_ID" -f "role=$ROLE_ID"
```

Expected: `201 Created`.

```json
{"id":"<grant-id>","application":{"id":"<application-id>","name":"CI pipeline"},"role":{"id":"<role-id>","name":"operator"},"content_type":null,"object_id":null}
```

### 4. Create the service token

[Create a service token](/use/iam/applications/service-tokens/) named `ci-deploy` with `command:create` and `command:read`. Set a future expiration and enter the justification `Run deployment checks from CI`. The example expiration below must still be in the future when you use it:

```bash
alpacon api -X POST /api/auth/service-tokens/tokens/ \
  -f name=ci-deploy -f "application=$APP_ID" \
  -f 'scopes[]=command:create' -f 'scopes[]=command:read' \
  -f expires_at=2027-01-01T00:00:00Z \
  -f 'justification=Run deployment checks from CI'
```

Command scopes are medium risk and require a nonempty `justification`. Creation always returns `202 Accepted` with a pending token for these scopes:

```json
{"id":"<token-id>","name":"ci-deploy","application":{"id":"<application-id>","name":"CI pipeline","service_type":"ci_cd","username":"ci-deploy"},"key":"alpst-…","enabled":false,"activation_state":"pending","scopes":["command:create","command:read"],"expires_at":"2027-01-01T00:00:00Z","rotation_policy":"90d","status":"pending_approval","risk_level":"medium","approval_request_id":"<approval-request-id>"}
```

Save `key` securely now; later GET responses do not return it. Save the token and approval request IDs too. The pending key cannot authenticate yet.

```bash
read -r -p 'Service token ID: ' TOKEN_ID
read -r -p 'Approval request ID: ' APPROVAL_REQUEST_ID
read -r -s -p 'Service token key: ' SERVICE_TOKEN
printf '\n'
```

### 5. Approve the token and confirm it is active

**5a. Approve in the web console:** only the web console can approve the token. Open `https://<workspace-console>/<workspace>/approvals?selected=$APPROVAL_REQUEST_ID`, replacing the console host and workspace placeholders with yours and `$APPROVAL_REQUEST_ID` with the ID saved in step 4. Or open **Pending approvals** and select the **Create a service token** row for this request. A Superuser or an approver for that request approves it. The requester may approve their own request unless an approval policy forbids it. Complete a fresh MFA check if requested. If the policy requires more approvals, wait for approval to complete.

**5b. Confirm it is active:** return to the CLI and read the token:

```bash
alpacon api -X GET "/api/auth/service-tokens/tokens/$TOKEN_ID/"
```

Expected: `200 OK` with `scope_usage` and no `key`:

```json
{"id":"<token-id>","activation_state":"active","enabled":true,"scopes":["command:create","command:read"]}
```

### 6. Allow the server

After approval, allow the server identified by `SERVER_ID` in the token's access rules. ACL writes accept only an approved, enabled token target. A pending or disabled target returns `400 does_not_exist` on `service_token`.

```bash
alpacon api -X POST /api/security/server-acl/ \
  -f "service_token=$TOKEN_ID" -f "server=$SERVER_ID"
```

Expected: `201 Created`.

```json
{"id":"<server-acl-id>","token":null,"service_token":"<token-id>","token_name":"ci-deploy","server":{"id":"<server-id>","name":"deploy-server","is_connected":true,"is_deleted":false}}
```

### 7. Allow the command

On Alpacon Cloud, open the token's page in the web console and select its **Access rules** tab (`?tab=acl`). Under **Pre-approved commands**, choose **Add** to open **Add command**. If the console says this list is managed by **Command ACL admins**, ask an admin for access. If the browser asks you to connect, choose **Connect**; this connection may be needed once.

In the dialog, set **Command** to `df -h`. Set **Run as** to `*` for any account, or enter `ci-deploy`. Set **Group** if you need a group restriction. Select servers under **Servers** to limit the rule to them; with no server selected, it applies to every server the token reaches. Confirm that the command appears in the list. Command ACL rows returned by the ACL API on Cloud do not decide which token commands run; writes to that API return `405 command_acl_moved`.

On a self-hosted deployment, allow `df -h` in the token's command access rules:

```bash
alpacon api -X POST /api/security/command-acl/ \
  -f "service_token=$TOKEN_ID" -f 'command=df -h'
```

Expected on a self-hosted deployment: `201 Created`.

```json
{"id":"<command-acl-id>","token":null,"service_token":"<token-id>","token_name":"ci-deploy","command":"df -h","username":"","groupname":""}
```

The empty `username` and `groupname` add no account restriction to this rule. The command still runs as the application's account from step 2.

### 8. Run the command with the token

Use the approved token to call the [commands API](/reference/api/commands/). No work session is required. Omit `username` and `groupname` so the command runs as the application's account. Send one connected, commissioned `server`; if the server is disconnected, the request returns `400 server_not_connected`; if it is not commissioned, the request returns `400 server_not_commissioned`.

A `400 does_not_exist` error on `server` means the server is missing from the token's server access rules or does not exist; check `SERVER_ID` and return to step 6. A `400 application_system_user_not_found` error means the application's account could not be provisioned: use a Linux server running a current agent, as required in step 2.

```bash
COMMAND_RESPONSE=$(curl --fail-with-body --silent --show-error \
  -X POST "$ALPACON_URL/api/events/commands/" \
  -H "Authorization: token=\"$SERVICE_TOKEN\"" \
  -H 'Content-Type: application/json' \
  -d "{\"server\":\"$SERVER_ID\",\"line\":\"df -h\",\"purpose\":\"Check disk space before deploying\"}")
printf '%s\n' "$COMMAND_RESPONSE" | jq .
COMMAND_ID=$(printf '%s' "$COMMAND_RESPONSE" | jq -er '.[0].id')
```

Expected: `201 Created`, with a one-item list. The first command after enabling provisioning waits behind account setup: `status` is `queued` and `run_after` identifies the earlier command. The initial status may differ as the command progresses; accepting it does not mean it has finished.

```json
[{"id":"<command-id>","line":"df -h","purpose":"Check disk space before deploying","username":"ci-deploy","groupname":"ci-deploy","status":"queued","run_after":["<account-setup-command-id>"],"request_channel":"service_token","work_session":null,"service_token":"<token-id>","application":"<application-id>"}]
```

### 9. Poll and read the command record

Poll the detail route until `status` is `completed`, `rejected` or `denied`:

```bash
HEADERS=$(mktemp)
BODY=$(mktemp)
while true; do
  HTTP_STATUS=$(curl --silent --show-error \
    --dump-header "$HEADERS" --output "$BODY" --write-out '%{http_code}' \
    "$ALPACON_URL/api/events/commands/$COMMAND_ID/" \
    -H "Authorization: token=\"$SERVICE_TOKEN\"") || break
  if [ "$HTTP_STATUS" = 429 ]; then
    RETRY_AFTER=$(awk 'tolower($1) == "retry-after:" { print $2 + 0 }' "$HEADERS")
    sleep "${RETRY_AFTER:-60}"
    continue
  fi
  if [ "$HTTP_STATUS" != 200 ]; then
    cat "$BODY"
    break
  fi
  COMMAND_RECORD=$(cat "$BODY")
  printf '%s\n' "$COMMAND_RECORD" | jq '{status, success, exit_code, result, elapsed_time, handled_at, rejection_cause, rejection_gate, rejected_at}'
  COMMAND_STATUS=$(printf '%s' "$COMMAND_RECORD" | jq -er '.status') || break
  case "$COMMAND_STATUS" in
    completed|rejected|denied) break ;;
  esac
  sleep 5
done
rm -f "$HEADERS" "$BODY"
```

The loop polls every 5 seconds (720 requests an hour, under the default 1000/hour limit); on a `429`, it waits the number of seconds in `Retry-After` and continues, so a command that waits a long time for approval still reaches its final record.

`awaiting_approval` waits for an approver in the web console; the loop keeps polling.

Expected for each GET: `200 OK`. While the command runs, `status` may be `delivered` or `running` and `handled_at` is `null`. A completed response includes:

```json
{"id":"<command-id>","status":"completed","success":true,"exit_code":0,"result":"<command output>","elapsed_time":0.12,"acked_at":null,"handled_at":"2026-10-24T12:00:00Z","verification_status":"approved","risk_level":"LOW"}
```

Read `success` first, then `exit_code`, `result`, `elapsed_time` and `handled_at`. `acked_at` can be `null` even when the command is completed. A stuck command that times out also ends as `completed`, but with `success: false`, `exit_code: null` and a timeout message in `result`.

For `status: "rejected"`, inspect `rejection_cause`, `rejection_gate` and `rejected_at`; for `status: "denied"`, inspect `result`. The separate `denied` field is available only on the list route, not this detail response. See [Command status](/reference/api/commands/#command-status) for waiting and running states.

The same detail response is the command record: it includes `delivered_at`, `acked_at`, `handled_at`, `verification_status`, `risk_level`, `service_token`, `service_token_name`, `application`, `application_name` and `request_channel`. See [Get command](/reference/api/commands/#get-command) for the fields.

## Alpacon features

Following documents describe the details about each collection of API.

Source: https://docs.alpacax.com/reference/api/
