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:
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. |
| 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. |
- 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 or through a service token.
- Use a service token for automated command execution and file transfers.
- Managing personal API tokens needs a signed-in session, not a token. See API tokens.
Some management routes require a signed-in session. Routes marked session-only refuse alpat- and alpst- keys. Follow 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.
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.
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 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 |
{
"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 can require approval | PATCH, PUT, POST |
204 No Content | Resource deleted | DELETE |
Approval does not always mean 202. Work-session creation 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 for field-level details. |
401 Unauthorized | Not authenticated. See 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. |
403 Forbidden | Permission denied. The credential is valid but may not do this. The body is a coded refusal. |
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. |
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.
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{
"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:
{
"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:
{
"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 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{
"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. 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 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:
{
"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:
{
"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{
"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: 864000864000 is an illustrative value (10 days); each response carries the seconds left in your own usage period.
{
"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.
{
"code": "workspace_enterprise_plan_required",
"gate": "plan"
}Refusal codes
At 402, 403, 405 and 429, every code below answers with a refusal 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.
For access decisions and recovery by action, see 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 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 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 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.
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_ID1. Create the application
Create an application named CI pipeline with service type ci_cd before configuring its server account:
alpacon api -X POST /api/iam/applications/ \
-f 'name=CI pipeline' -f service_type=ci_cdExpected: 201 Created.
{"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:
read -r -p 'Application ID: ' APP_ID2. 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:
alpacon api -X PATCH "/api/iam/applications/$APP_ID/" \
-f username=ci-deploy -F provisioning_enabled=trueExpected: 200 OK, with the updated application:
{"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.
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:
alpacon api -X GET /api/rbac/roles/ -f name=operatorExpected: 200 OK, with results containing the role:
{"results":[{"id":"<role-id>","name":"operator"}]}Save its id as ROLE_ID:
read -r -p 'Operator role ID: ' ROLE_ID3b. 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".
alpacon api -X POST /api/rbac/application-roles/ \
-f "application=$APP_ID" -f "role=$ROLE_ID"Expected: 201 Created.
{"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 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:
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:
{"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.
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:
alpacon api -X GET "/api/auth/service-tokens/tokens/$TOKEN_ID/"Expected: 200 OK with scope_usage and no key:
{"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.
alpacon api -X POST /api/security/server-acl/ \
-f "service_token=$TOKEN_ID" -f "server=$SERVER_ID"Expected: 201 Created.
{"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:
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.
{"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. 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.
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.
[{"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:
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:
{"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 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 for the fields.
Alpacon features
Following documents describe the details about each collection of API.