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

Note

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"
TokenPrefixUse it for
Service tokenalpst-Automation. Commands and file transfers run through a service token under its access-control rules. Issue one for an application.
Personal API tokenalpat-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:

CodeWhen
auth_token_missingNo credential, or a personal API token that doesn’t exist, is disabled or expired, or belongs to an inactive user
auth_authentication_failedA service token that doesn’t exist, is disabled, pending or expired, or belongs to a deleted application
auth_ip_not_allowedA service token used from outside its allowed IP ranges
auth_access_outside_allowed_timeA service token used outside its allowed days and hours
auth_mfa_requiredThe action needs a recent MFA check. A 401 body also carries source, naming the check
auth_token_mfa_missingA 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_revokedA 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

MethodUsage
GETList or retrieve resources
POSTCreate a resource
PUTReplace a resource entirely
PATCHUpdate specific fields of a resource
DELETEDelete 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:

ParameterMeaning
pageThe page number, from 1
page_sizeResults 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:

CodeMeaningTypical method
200 OKRequest succeededGET, PUT, PATCH
201 CreatedResource createdPOST
202 AcceptedAccepted for approval or asynchronous processing; the response describes what is pending. For example, a token scope change can require approvalPATCH, PUT, POST
204 No ContentResource deletedDELETE

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:

CodeMeaning
400 Bad RequestInvalid request data. See error messages for field-level details.
401 UnauthorizedNot authenticated. See authentication for the codes.
402 Payment RequiredThe workspace’s plan, or a workspace extension it hasn’t enabled, doesn’t cover this action. See refusals.
403 ForbiddenPermission denied. The credential is valid but may not do this. The body is a coded refusal.
404 Not FoundThe resource doesn’t exist, or the credential can’t see it. The code is api_not_found.
405 Method Not AllowedHTTP method not supported on this endpoint.
409 ConflictThe request is valid, but the resource’s current state refuses it. Retrying later may succeed.
429 Too Many RequestsRate limited. Carries a Retry-After header. See rate limits.
503 Service UnavailableA dependency couldn’t answer: auth_verification_unavailable while checking a credential, or api_search_unavailable while reading session history. Retry the request.
500 Server ErrorInternal 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
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:

StatusCodeWhen
400api_invalid_dataThe request body can’t be parsed, such as malformed JSON. A few endpoints also use it for data they can’t accept
406api_not_acceptableThe Accept header asks for a format the endpoint can’t return
415api_unsupported_media_typeThe Content-Type isn’t one the endpoint accepts. Send the type the endpoint documents, usually application/json
500api_unknown_errorAn 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.

CodeMeaningWhat to do
rbac_application_role_global_requiredThe application has no workspace-wide role, so a rule-scoped grant is refusedGive the application a workspace-wide role that covers the required token scopes, then retry
rbac_application_rule_roles_remainRule-scoped roles remain, so the application’s last workspace-wide role cannot be removedRemove 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

Note

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:

ValueMeaning
work_sessionThe request exceeds what the active work session’s scope allows
roleThe caller’s RBAC role doesn’t grant this permission
token_scopeThe API or service token doesn’t hold a scope this action requires
server_reachThe credential’s server access-control rules don’t cover this server
command_aclThe command isn’t pre-approved for this credential
file_aclThe token’s file access rules refused this transfer: its path, account and transfer direction
approvalThe action needs a human approval that hasn’t been granted
presenceThe action needs a fresh MFA/step-up proof
riskRisk-based execution control refused the action
sudoThe sudo request was refused
planThe 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
rateA time-based rate limit refused the request (429)

missing values:

ValueMeaning
command_preapprovalA pre-approval for this command would have let it through
file_preapprovalA file access rule allowing this path, account and transfer direction would have let it through
approvalAn approval would have let it through
presenceFresh presence proof would have let it through
work_sessionAn active work session would have let it through
planA higher plan tier would have let it through
roleA role granting this permission would have let it through
approved_accountRunning as the account the work session’s approval covered would have let it through
other_approverAn approver other than the requester would have let it through
allowed_agentAn 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

Note

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 valueWhat it names
serverRegistered servers
userWorkspace members, including pending invitations
applicationApplications (identities for automation)
webshWebsh session time in the current usage period
webftpWebFTP transfer volume in the current usage period
websh-shareTime spent sharing Websh sessions with other users in the current usage period
workspaceCreating 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 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

CodeMeaningWhat 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

CodeMeaningWhat 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

CodeMeaningWhat 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

CodeMeaningWhat 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

CodeMeaningWhat 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

CodeMeaningWhat 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

CodeMeaningWhat 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.

CodeMeaningWhat 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.

CodeMeaningWhat 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.

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 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.

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 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. 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 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.

Last updated: