---
title: "Work sessions"
description: "Create and manage Alpacon work sessions through the API. Follow approval, update and extension responses, read timelines and records, and handle errors."
---

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

# Work sessions

**Endpoint**: `/api/work-sessions/sessions/`

Create a bounded session for a person or a token, follow its approval status, and read its activity. A service token cannot create a work session: a person creates one assigned to that token with `assigned_service_token`. Service-token automation does not require a work session.

## Access

A Superuser can view and manage every session. Otherwise, the assigned user, the owner of the assigned personal token, or the owner of the assigned service token's application can manage it. A server or cluster owner or manager can view sessions involving their servers or clusters; their timeline, recordings and live snapshot contain only activity on those targets.

Personal API tokens need the corresponding `work_session` scope and the owner's permission on the session. Creating through a personal API token sets `requester_type` to `agent`. An agent cannot use the `websh` or `kube_exec` presets. Service-token reads are limited by session visibility, token scopes and the audit window.

Approval and rejection work only from the web console. Personal tokens, service tokens, CLI and MCP requests receive `403 permission_denied`. Revocation is Superuser-only; a personal token can revoke only when its owner is a Superuser. Cancellation is limited to the creator or a Superuser, not the assignee merely because they are assigned. Tokens with granular scopes cannot call cancel, live snapshot or recommendation acknowledgment.

## Routes

All routes below begin with `/api/work-sessions/sessions/`. `{id}` is the work session UUID.

| Method | Suffix | Success | Token scope or access |
|--------|--------|---------|-----------------------|
| `GET` | (none) | `200`, paginated sessions | `work_session:read` |
| `POST` | (none) | `201`, session | `work_session:create`; no service tokens |
| `GET` | `{id}/` | `200`, session | `work_session:read` |
| `PUT`, `PATCH` | `{id}/` | `200` or `202`, session | `work_session:update`; manage permission |
| `POST` | `{id}/activate/` | `200`, session | `work_session:activate`; manage permission |
| `POST` | `{id}/complete/` | `200`, session | `work_session:complete`; manage permission |
| `POST` | `{id}/revoke/` | `200`, session | `work_session:revoke`; Superuser |
| `POST` | `{id}/cancel/` | `200`, session | Creator or Superuser signed in; tokens with granular scopes are refused |
| `POST` | `{id}/approve/` | `200`, session or pending vote | Web console approver |
| `POST` | `{id}/reject/` | `200`, session | Web console approver |
| `POST` | `{id}/extend/` | `200` or `202`, session | `work_session:extend`; manage permission |
| `GET` | `{id}/timeline/` | `200`, timeline | `work_session:timeline` |
| `GET` | `{id}/records/` | `200`, recording chunks | `work_session:timeline`; paid plan |
| `GET` | `{id}/live_snapshot/` | `200`, snapshot | Signed-in session with view permission; tokens with granular scopes are refused |
| `POST` | `{id}/analyze/` | `200` or `202` | `work_session:analyze`; manage permission and paid plan |
| `POST` | `{id}/acknowledge-recommendations/` | `200`, acknowledgment time | Assigned person signed in; tokens with granular scopes are refused |
| `POST` | `suggest-scopes/` | `200`, suggestions | `work_session:suggest_scopes` |
| `POST` | `resolve-target-account/` | `200`, account preview | `work_session:resolve_target_account` and `proc:read`; no service tokens |
| `POST` | `candidate-target-accounts/` | `200`, candidates | `work_session:candidate_target_accounts` and `proc:read`; no service tokens |

`DELETE` is unsupported and returns `405`. Scheduled sessions activate on their own; `activate` is only needed to start an approved session early.

## Status flow

| Transition | Trigger and caller |
|------------|--------------------|
| Create → `active` | No approval needed and no future start time |
| Create → `approved` | No approval needed, with a future `starts_at` |
| Create → `pending` | Approval needed |
| `pending` → `active` or `approved` | An approver settles approval in the web console; future sessions become `approved` |
| `pending` → `rejected` | An approver rejects in the web console |
| `pending` → `cancelled` | Creator or Superuser cancels, or the request reaches its approval deadline or expiry |
| `approved` → `active` | Start time arrives, or a caller with manage permission calls activate |
| `active` → `completed` | A caller with manage permission calls complete |
| `approved` or `active` → `revoked` | A Superuser revokes |
| `approved` or `active` → `expired` | Expiry arrives |

Completed, expired, revoked, rejected and cancelled sessions cannot be modified. Ending a session also cancels pending modifications and closes its live connections. With a `duration`, `expires_at` stays null until activation, then becomes `started_at + duration`.

## Create a session

```http
POST /api/work-sessions/sessions/
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `title` | string | No | Up to 128 characters; default empty |
| `description` | string | Yes | Purpose, up to 2,048 characters |
| `requester_type` | string | Yes | `user` or `agent`; machine channels force `agent` |
| `scopes` | array[string] | Yes | Non-empty presets: `websh`, `tunnel`, `editor`, `webftp`, `command`, `sudo`, `kube`, `kube_exec` |
| `servers` | array[UUID] | One target kind | Up to 100 visible server IDs; objects containing `id` are also accepted |
| `clusters` | array[UUID] | One target kind | At most one cluster; requires `kube` and only Kubernetes presets, where Kubernetes is available |
| `target_account_username` | string | No | Up to 128 printable ASCII characters, with no whitespace, `:` or `/`; defaults to the assigned person's username. Not accepted on token-held or cluster sessions |
| `assigned_user` | user ID | No | Defaults to the caller |
| `assigned_token` | UUID | No | Assign to a personal API token instead of a user |
| `assigned_service_token` | UUID | No | Assign to a service token instead of a user |
| `duration` | duration | One time limit | Positive duration, such as `01:00:00`, within the workspace's maximum session duration |
| `expires_at` | datetime | One time limit | Future ISO 8601 expiry within the maximum duration |
| `starts_at` | datetime | No | Future start time before expiry; default null means activate on approval |
| `sudo_policies` | array[object] | No | Up to 10 policies; requires the `sudo` preset |

Choose either servers or clusters, exactly one assignee, and either `duration` or `expires_at`. A non-Superuser may assign only to themselves, their own personal token, or a service token of an application they own. The default maximum session duration is four hours. `request_channel` and `declared_agent` are read-only.

Each sudo policy has non-empty `commands` (each up to 512 characters), optional `users`, `allow_bypass_mfa` (default false, allowing the session approval to take the place of MFA for sudo), `reason` (up to 1,024 characters) and optional `valid_until`. A non-Superuser may name only the assigned user in `users`. Changes to sudo policies where the session approval takes the place of MFA can require approval or be refused with the errors below.

For example, use a personal token with `work_session:create` to request an agent command session for its owner to use from a signed-in CLI or MCP session. The personal token cannot run commands in it: personal tokens cannot be issued command execution scopes.

```bash
curl -X POST "https://your-workspace.us1.alpacon.io/api/work-sessions/sessions/" \
  -H "Authorization: token=\"alpat-xxxxxxxxxxxxxxxxxx\"" \
  -H "Content-Type: application/json" \
  -d '{"description":"Check disk space before deployment","requester_type":"agent","scopes":["command"],"servers":["7e3984de-49ab-4cc6-bcdf-21fbd35858b8"],"duration":"01:00:00"}'
```

Every successful create returns `201`, including when approval is required. In that case the session has `status: "pending"` and an `approval_request_id`. Poll the detail route until its status changes; a successful HTTP response does not mean the session is ready to use.

```bash
curl "https://your-workspace.us1.alpacon.io/api/work-sessions/sessions/550e8400-e29b-41d4-a716-446655440000/" \
  -H "Authorization: token=\"alpat-xxxxxxxxxxxxxxxxxx\""
```

The polling token needs `work_session:read`. On the Free plan, delegation that would prevent the creator from reading the resulting session returns `402 workspace_plan_update_required`.

## Update or extend

```http
PATCH /api/work-sessions/sessions/{id}/
PUT /api/work-sessions/sessions/{id}/
POST /api/work-sessions/sessions/{id}/extend/
```

For a pending session, update `title`, `description`, `scopes`, `servers`, `clusters`, `target_account_username`, `starts_at` or `expires_at`. The change applies directly and the approval request is reassessed. The assignee cannot change, and a declared account cannot be cleared.

For an approved or active session, update `title`, `description`, `scopes`, `servers`, `target_account_username` or `sudo_policies`. Send the complete desired sudo policy list; existing entries can include their `id`. Removals and restrictions apply immediately, as does a title change from the web console. Changes requiring approval include description changes, added scopes or servers, wider sudo policies and account changes. Only one modification request can be pending.

An update returns `200` when applied directly, or `202` with the full session when a modification is pending. Its `pending_modification_request` has this shape:

```json
{
  "id": "9b52a7c4-13de-48f6-a01b-7f3e5d982c44",
  "changes": {},
  "added_at": "2026-10-24T10:00:00Z",
  "can_cancel": true
}
```

To extend a session, send a later `expires_at` and a non-blank `reason` of up to 2,048 characters. It must be approved or active, its start time must have arrived, and it must not have expired. The new expiry must remain within the workspace's maximum duration measured from now.

```json
{
  "expires_at": "2026-10-24T12:00:00Z",
  "reason": "Finish deployment verification"
}
```

An extension returns `200` when applied directly or `202` with the full session when approval is needed. Only one extension request can be pending. Its `pending_extension_request` is:

```json
{
  "id": "9b52a7c4-13de-48f6-a01b-7f3e5d982c44",
  "requested_expires_at": "2026-10-24T12:00:00Z",
  "expires_at": "2026-10-24T11:00:00Z",
  "reason": "Finish deployment verification",
  "added_at": "2026-10-24T10:00:00Z"
}
```

Here `expires_at` is the request deadline, bounded by the session's current expiry. Read the detail response's `can_extend`, `extend_blocked_reason`, `max_extendable_to`, `extension_request_deadline` and `extension_ends_with_session` before requesting an extension. These hints are null in list responses.

For approval-related changes on this page, only updates and extensions return `202`; creation returns `201` even when pending. Analysis uses `202` separately to acknowledge asynchronous work.

## Approval and other actions

Approve and reject through the web console as an eligible approver. A policy can prevent the requester from approving their own request. If fresh MFA presence is required, these routes return `400 auth_mfa_required` without a `source` field.

Approval accepts optional `adjusted_scopes` (non-empty valid presets), `adjusted_servers` (up to 100 UUIDs, only narrowing), `adjusted_target_account_username` (non-blank, only narrowing), and either `adjusted_duration` (positive whole seconds) or `adjusted_expires_at` (future). Optional `recommendations` is an array of objects containing `id`, `text`, `severity` (`low`, `medium`, `high`), `source` (`ai_suggested`, `ai_edited`, `admin_added`) and `auto_checkable` (boolean). `sudo_auto_approve: true` is refused; `sudo_risk_ceiling` is accepted but has no effect.

An approver without visibility of the session receives `404` here and must use `POST /api/approvals/approvals/{id}/approve/` for the approval request instead.

Approval returns the session when settled. If more votes are needed, it returns `200` with:

```json
{"status":"pending","approvals_received":1,"approvals_required":2}
```

Reject and cancel return the session. Cancel requires a pending session. Complete requires an active session and starts analysis. Revoke requires an approved or active session. Activate requires an approved, unexpired session and also succeeds if it is already active.

`POST {id}/acknowledge-recommendations/` is idempotent and returns `{"recommendations_acknowledged_at":"2026-10-24T10:00:00Z"}`. Only the assigned person can acknowledge.

`POST {id}/analyze/` requires `completed`, `expired` or `revoked`. It returns `202 {"status":"accepted","work_session":"SESSION_ID"}`, or `200` with `status: "already_exists"`. `?force=true` retries a stuck pending or processing analysis once it is old enough to retry. This route requires a paid plan.

## List and read sessions

```http
GET /api/work-sessions/sessions/
GET /api/work-sessions/sessions/{id}/
```

| Parameter | Description |
|-----------|-------------|
| `status` | Comma-separated session statuses |
| `request_channel` | Comma-separated `cli`, `mcp`, `api_token`, `service_token`, `unknown`, `unknown_client`, `web` |
| `scopes` | Comma-separated presets; matches any overlap |
| `requester_type` | Exact requester type |
| `server`, `cluster`, `assigned_user` | Filter by UUID |
| `start`, `end` | Inclusive ISO 8601 bounds on creation time |
| `has_attention` | Boolean; also enables live summaries |
| `analysis_status` | `pending`, `processing`, `completed` or `failed` |
| `analysis_ended_at__gte`, `analysis_ended_at__lt` | Inclusive lower and exclusive upper bounds on analysis completion |
| `search` | Search title, description, server and cluster names |
| `ordering` | `added_at`, `expires_at`, `starts_at`, `started_at`, `status`, `attention`; default `-added_at`. `attention` must come first and enables live summaries |
| `include` | Set to `live_summary` to include it per row |
| `page`, `page_size` | Integer pagination; default size 15, maximum 100, larger values clamped |

Unknown statuses, channels or presets return `400 invalid_input`.

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

`results` contains session objects. `next` and `previous` are integer page numbers or `null`.

### Session fields

| Fields | Description |
|--------|-------------|
| `id`, `title`, `description`, `status` | Identity, purpose and current state |
| `requester_type`, `request_channel`, `requester_type_verified`, `declared_agent` | Requester and channel information |
| `scopes`, `servers`, `clusters` | Allowed activities and targets. Each server contains `id`, `name`, `os`, `platform`, `is_connected`, `is_deleted` |
| `target_account_username`, `target_account_is_self`, `account_flag` | Target account information |
| `created_by`, `assigned_user`, `assigned_token`, `assigned_service_token`, `assigned_credential` | Creator and assignee; `assigned_credential` is null unless you can manage the session |
| `starts_at`, `expires_at`, `duration`, `started_at`, `completed_at` | Requested and actual time window |
| `approval_request_id`, `admission_decision`, `adjustments` | Approval and adjustments |
| `pending_modification_request`, `pending_extension_request` | Pending changes in the shapes above |
| `sudo_policies`, `proposed_sudo_policies`, `sudo_auto_approve`, `sudo_risk_ceiling`, `sudo_posture` | Sudo configuration. Bound policies contain `id`, `commands`, `allow_bypass_mfa`, `reason`, `valid_from`, `valid_until` |
| `ai_suggestion`, `recommendations`, `recommendations_acknowledged_at` | Suggestions and acknowledgment |
| `analysis`, `can_analyze`, `can_manage` | Analysis is null or an object with `id`, `status`, `risk_score`; available actions |
| `can_extend`, `extend_blocked_reason`, `max_extendable_to`, `extension_request_deadline`, `extension_ends_with_session` | Extension hints, populated on detail and live snapshot |
| `live_summary` | Included when requested |
| `added_at`, `updated_at` | Creation and update timestamps |

## Timeline and recordings

These routes use their own cursor formats, not the integer pagination above.

### Timeline

```http
GET /api/work-sessions/sessions/{id}/timeline/
GET /api/work-sessions/sessions/{id}/timeline/?cursor=&page_size=100
```

Without `cursor`, the response is `{"results":[...]}` for the whole session. `include_records` defaults to true; `0`, `false` or `no` turns recording inclusion off. Embedded recordings are limited to 2 MiB per response and 128 KiB per Websh session. The Free plan returns only the opening of each recording. `page_size` has no effect in this form.

Include `cursor`, even empty, to select forward cursor pagination:

```json
{"next":"OPAQUE_CURSOR","results":[]}
```

Pass the returned value as `cursor` for the next request; stop when `next` is null. There is no `count` or `previous`. `page_size` defaults to 100 and must be an integer from 1 to 500; invalid or out-of-range values return `400 invalid_input`. A cursor from a different workspace, session or access scope returns `400 api_invalid_cursor`.

Items share `type`, `timestamp`, `id` and `server_id`. Types are `command`, `websh_session`, `tunnel_session`, `websh_record`, `ftp_session`, `file_upload`, `file_download`, `sudo_grant` and `kube_request`. They are chronological; ties follow that type order. Commands contain `masked_output_summary` (up to 200 characters) and `output_truncated`. Cursor pages contain no recording bytes; `websh_session` items include `record_chunks_total` so you can fetch chunks separately.

### Recording chunks

```http
GET /api/work-sessions/sessions/{id}/records/?session={websh_session_id}
```

`session` is a required Websh session UUID belonging to this work session. Tunnel sessions are not accepted. A paid plan is required.

```json
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [{"added_at":"2026-10-24T10:00:00Z","record":"masked terminal output"}]
}
```

`next` and `previous` are opaque cursors or null. Send the chosen cursor in `cursor`; `cursor=last` starts at the last page. `page_size` defaults to 8 and is capped at 16. Invalid values use the default, and larger values are clamped. Each response also stops at 2 MiB, so a page may contain fewer chunks; continue with `next`. Expired cursors return `400 api_cursor_expired`; invalid cursors return `400 api_invalid_cursor`. If history search is unavailable, the route returns `503 api_search_unavailable`.

### Live snapshot

```http
GET /api/work-sessions/sessions/{id}/live_snapshot/
```

Use a signed-in session with view permission. `since` accepts an ISO 8601 datetime with an offset. `limit` defaults to 200 and is capped at 200; a non-integer returns `400 invalid_input`. The response contains `work_session`, `last_seq`, `snapshot_taken_at`, `status_history`, `commands`, `websh_sessions`, `sudo_grants` and `ftp_events`.

## Planning helpers

| Route | Request | Response |
|-------|---------|----------|
| `POST /api/work-sessions/sessions/suggest-scopes/` | Required `description` (up to 2,048 characters), non-empty `servers`; optional `requester_type` (default `user`), `proposed_sudo_policies`, `include_improved_description` (default false) | `suggested_scopes`, `risk_level`, `reasoning`, `improved_description`, `sudo_policy_assessments` |
| `POST /api/work-sessions/sessions/resolve-target-account/` | Required `servers`; optional `assigned_user`, `target_account_username` (defaults to the assignee's username) | Account preview: `version`, `account`, `is_self`, `is_own_username`, `lane`, `reason`, `role_reach`, `servers` |
| `POST /api/work-sessions/sessions/candidate-target-accounts/` | Required `servers`; optional `assigned_user` | `server_count`, `accounts` |

The account helpers refuse service tokens. A non-Superuser can name only themselves in `assigned_user`. Personal tokens need both the route's `work_session` scope and `proc:read`. Scope suggestions return `400 work_session_ai_unavailable` when AI is unavailable.

## Audit window and plans

The audit window is 7 days on Free, 120 days on Essentials and 365 days on Enterprise. List results omit older sessions. Free also limits the list to your own sessions. On Essentials and Enterprise, you can read organization-wide history within your normal visibility; service tokens have organization audit scope within their own visibility and the plan's window.

For detail, timeline, recordings and live snapshot, a Free-plan request for another person's session returns `404 api_not_found`. An otherwise accessible session outside the audit window returns `402 workspace_plan_update_required`. Recordings and analysis require a paid plan even inside the window.

## Errors

| Status | Code | Meaning |
|--------|------|---------|
| `400` | `work_session_principal_not_supported` | Service token used for create or either account helper |
| `400` | `work_session_invalid_scope`, `work_session_scope_not_allowed` | Invalid preset or preset unavailable to agents |
| `400` | `work_session_no_servers`, `work_session_mixed_targets`, `work_session_too_many_clusters`, `cluster_not_found` | Invalid targets |
| `400` | `server_invalid`, `server_not_identified`, `server_not_found`, `server_too_many`, `server_access_denied` | Invalid, excessive or inaccessible servers |
| `400` | `work_session_multiple_assignees`, `work_session_assignee_not_allowed` | Conflicting assignees or assignment not permitted |
| `400` | `work_session_duration_or_expires_required`, `work_session_duration_and_expires_exclusive`, `work_session_duration_not_positive` | Invalid duration or expiry choice |
| `400` | `work_session_expired`, `work_session_starts_at_in_past`, `work_session_starts_at_after_expires`, `work_session_expires_exceeds_max_ttl` | Invalid time window |
| `400` | `work_session_sudo_policy_without_scope`, `work_session_sudo_policy_user_not_assigned`, `work_session_sudo_policy_too_many`, `work_session_sudo_policy_id_not_bound` | Invalid sudo policy request |
| `400` | `work_session_target_account_invalid`, `work_session_target_account_not_applicable`, `work_session_target_account_clear_forbidden`, `work_session_target_account_absent`, `work_session_target_account_unavailable`, `work_session_target_account_not_reachable`, `target_account_other_person` | Target account invalid, unavailable, inaccessible or belongs to another person |
| `400` | `work_session_admission_denied` | Session refused by policy |
| `400` | `work_session_not_pending`, `work_session_not_active`, `work_session_not_approved`, `work_session_not_started`, `work_session_not_terminal`, `work_session_not_modifiable` | Action requires a different state |
| `400` | `work_session_mod_no_changes`, `work_session_mod_already_pending` | No modification or another modification pending |
| `400` | `work_session_extend_not_later`, `work_session_extension_reason_required`, `work_session_extension_already_pending` | Invalid extension or another extension pending |
| `400` | `work_session_invalid_recommendation_item`, `work_session_sudo_loosen_forbidden` | Invalid approval adjustments |
| `400` | `approval_request_already_reviewed_or_cancelled` | Approval already decided or cancelled |
| `400` | `auth_mfa_required` | Approve or reject requires fresh MFA presence; no `source` field |
| `400` | `work_session_not_assignee` | Only the assigned person can acknowledge recommendations |
| `400` | `work_session_ai_unavailable` | Scope suggestions unavailable |
| `400` | `invalid_input`, `api_invalid_cursor`, `api_cursor_expired` | Invalid query, recording session or cursor |
| `400` | `user_not_found` | Requester of a pending edit cannot be found |
| `401` | `auth_token_missing` | Authentication missing |
| `402` | `workspace_plan_update_required` | Paid-plan feature, audit window or Free-plan delegation limit |
| `403` | `api_token_scope_missing` | Missing token scope; carries `gate: "token_scope"` |
| `403` | `permission_denied` | Insufficient session permission or wrong approval channel |
| `403` | `approval_request_not_approver`, `approval_requester_may_not_approve`, `approval_no_other_approver` | Approval permission or available approver requirement |
| `403` | `work_session_agent_not_allowed` | Declared agent refused |
| `403` | `work_session_sudo_bypass_console_only`, `sudo_policy_bypass_agent_session_needs_presence` | Request for a sudo policy where the session approval takes the place of MFA requires the console or human presence |
| `403` | `workspace_suspended` | Write in a suspended workspace; approve and reject are exempt |
| `404` | `api_not_found` | Session or recording not visible |
| `405` | Unsupported method | Session deletion is unsupported |
| `503` | `api_search_unavailable` | Recording search unavailable |

See the [API reference](/reference/api/) for authentication and error envelopes.

Source: https://docs.alpacax.com/reference/api/work-sessions/
