REST API (v1) reference¶
The AWC control plane exposes a public HTTPS JSON API at /v1/*. This page
lists every endpoint, its request and response fields, its auth and
idempotency rules, and the error model. Where the shipped build lags the
design, the page says so.
The host-local and internal gRPC methods (mount, unmount, checkpoint, lease renew, publish) are not REST. See gRPC services.
Transport¶
| Property | Value |
|---|---|
| Wire format | JSON (Content-Type: application/json) |
| Transport | HTTPS (spec §13.1) |
| Base path | /v1 |
| Framework | axum |
| Body decoding | serde_json; a malformed body returns INVALID_ARGUMENT |
| Timestamps | RFC 3339 UTC strings, for example 2026-08-05T15:00:00Z |
Authentication¶
Every endpoint except GET /v1/health requires a bearer token (spec §13.2,
§16.4).
| Header | Required | Value |
|---|---|---|
Authorization |
Yes (except health) | Bearer <token> |
The control plane branches on the token shape (spec §13.2):
| Principal kind | Token form | How it is verified |
|---|---|---|
USER |
OIDC id_token |
OIDC verifier (awc-auth) |
SERVICE_ACCOUNT |
awcsat1_<b64url(payload)>.<b64url(sig)> |
HMAC against the configured service-token key |
A token that starts with awcsat1_ is parsed as a service-account token. Any
other bearer token is treated as an OIDC id_token. If service-account tokens
are not configured, an awcsat1_ token returns UNAUTHENTICATED.
Authorization is checked per request against the principal's project role (spec
§16.4). A principal with no membership for the target project receives
PERMISSION_DENIED. Cross-tenant access returns PERMISSION_DENIED without
leaking existence (spec §16.5).
Common headers¶
| Header | Direction | Purpose | Spec |
|---|---|---|---|
Authorization |
Request | Bearer credential (see above) | §13.2 |
idempotency-key |
Request | Deduplicates a mutation (mutations only) | §13.1 |
x-request-id |
Both | Tracing id; honored inbound, minted if absent, echoed outbound | §13.1 |
Idempotency¶
All mutation endpoints accept the idempotency-key header (spec §13.1). A
retry that carries the same key replays the committed result.
| Property | Value |
|---|---|
| Replay window | 24 hours (IDEMPOTENCY_TTL) |
| Key scope | tenant_id + principal + operation + key |
| Conflict response | IDEMPOTENCY_CONFLICT (HTTP 409) |
| Idempotent operations | create import session, complete import, create workspace, fork workspace, issue mount ticket |
POST :batchCheckObjects and all GET endpoints are reads. They do not read
the idempotency-key header.
Rate limiting¶
Every authenticated request passes through a per-principal rate limiter keyed
by tenant_id/subject (spec §16.10). When the bucket is empty the request
returns RESOURCE_EXHAUSTED (HTTP 429).
Resource identifiers¶
Human-facing ids are opaque, prefix-qualified ULIDs of the form
<prefix>_<ulid> (spec §9.1).
| Type | Prefix | Appears in |
|---|---|---|
TenantId |
ten_ |
resource records |
ProjectId |
prj_ |
requests and records |
SnapshotId |
snap_ |
snapshots, workspaces |
WorkspaceId |
ws_ |
workspaces |
ImportSessionId |
import_ |
import sessions |
HostId |
host_ |
mount tickets |
LeaseId |
lease_ |
writer leases |
MountSessionId |
mount_ |
mount sessions |
A mount ticket id uses tkt_. The opaque signed ticket string uses the
awcmt1_ prefix (see Mount tickets).
Endpoints¶
The status column marks what the shipped build serves. "Implemented" means the route is served. "Specified" means a wire contract defines it but the router does not serve it.
Import sessions¶
| Method | Path | Status | Spec |
|---|---|---|---|
POST |
/v1/import-sessions |
Implemented | §13.3 |
POST |
/v1/import-sessions/{id}:batchCheckObjects |
Implemented | §13.4 |
POST |
/v1/import-sessions/{id}:complete |
Implemented | §13.5 |
The verb suffix travels in the same path segment, for example
import_01J...:complete. The router captures the whole segment and splits on
:. An unknown suffix returns NOT_FOUND.
POST /v1/import-sessions¶
Opens a snapshot import session. Idempotent.
Request fields (CreateImportSessionRequest):
| Field | Type | Required | Notes |
|---|---|---|---|
project_id |
ProjectId |
Yes | |
source_type |
enum | Yes | directory, git, tar, checkpoint |
normalization_policy |
enum | Yes | source-normalized, environment-preserving |
name |
string | No | |
labels |
map | No | Omitted when empty |
Response (CreateImportSessionResponse):
| Field | Type | Notes |
|---|---|---|
id |
ImportSessionId |
import_<ulid> |
state |
SnapshotState |
UPLOADING |
upload_endpoint |
string | Opaque capability scoped to the tenant CAS prefix and session |
expires_at |
timestamp |
The upload capability is limited to the tenant CAS prefix and the session (spec §13.3).
POST /v1/import-sessions/{id}:batchCheckObjects¶
Checks which referenced objects are missing from the CAS. A read; not idempotency-keyed.
Request fields (BatchCheckObjectsRequest):
| Field | Type | Required |
|---|---|---|
objects |
array of ObjectRef |
Yes |
ObjectRef:
| Field | Type | Notes |
|---|---|---|
digest |
Digest |
sha256:<hex> |
size_bytes |
u64 | |
kind |
enum | BLOB, FILE, TREE, SNAPSHOT |
Response (BatchCheckObjectsResponse):
| Field | Type | Notes |
|---|---|---|
missing |
array of Digest |
Digests absent from the CAS |
upload_urls |
map | Optional. Digest string to presigned upload URL. Omitted when not offered |
The service MAY return presigned upload URLs (spec §13.4). The shipped
in-memory and database backends report missing only.
POST /v1/import-sessions/{id}:complete¶
Completes the import and starts closure validation. Idempotent.
Request fields (CompleteImportRequest):
| Field | Type | Required |
|---|---|---|
snapshot_manifest_digest |
Digest |
Yes |
declared_logical_bytes |
u64 | Yes |
declared_file_count |
u64 | Yes |
Response (CompleteImportResponse):
| Field | Type | Notes |
|---|---|---|
snapshot_id |
SnapshotId |
snap_<ulid> |
state |
SnapshotState |
VALIDATING |
The snapshot becomes mountable only after closure validation sets it to
READY (spec §13.5).
Snapshots¶
| Method | Path | Status | Spec |
|---|---|---|---|
GET |
/v1/snapshots |
Implemented | §13 (list) |
GET |
/v1/snapshots/{id} |
Implemented | §13 (get) |
GET |
/v1/snapshots/{left}:diff?right={right} |
Implemented (JSON only) | §13.14 |
GET /v1/snapshots¶
Lists snapshot metadata. The project_id query parameter is required.
Query parameters (ListSnapshotsRequest):
| Parameter | Type | Required | Notes |
|---|---|---|---|
project_id |
ProjectId |
Yes | |
state |
SnapshotState |
No | Client-side filter |
parent_snapshot_id |
SnapshotId |
No | Children of this snapshot |
Response (ListSnapshotsResponse):
| Field | Type |
|---|---|
snapshots |
array of Snapshot |
The state and parent_snapshot_id filters are applied in the handler after
the project listing, because the persistence port lists by project and tenant
only.
GET /v1/snapshots/{id}¶
Returns one snapshot record. An unknown id returns NOT_FOUND. A known id
that belongs to another tenant returns PERMISSION_DENIED (the snapshot
store fetches by id, then the handler enforces tenant match).
Snapshot resource:
| Field | Type | Notes |
|---|---|---|
id |
SnapshotId |
|
tenant_id |
TenantId |
|
project_id |
ProjectId |
|
manifest_digest |
Digest |
Content identity of the snapshot manifest |
root_tree_digest |
Digest |
Root tree the manifest references |
state |
SnapshotState |
See Snapshot states |
logical_bytes |
u64 | Logical, de-duplicated-summed size |
file_count |
u64 | |
parent_snapshot_id |
SnapshotId |
Optional. None for an imported root |
source |
enum | directory, git, tar, checkpoint |
normalization_policy |
enum | source-normalized, environment-preserving |
created_by |
string | |
created_at |
timestamp | |
labels |
map | Optional. Omitted when empty |
GET /v1/snapshots/{left}:diff¶
Compares two snapshot root trees and returns an inline change list. The
right parameter is required.
Query parameters (DiffRequest):
| Parameter | Type | Required | Notes |
|---|---|---|---|
right |
SnapshotId |
Yes | |
format |
enum | No | Default json |
The shipped handler accepts format=json only. format=patch-bundle and
format=tar return INVALID_ARGUMENT (spec §13.14).
Response (DiffResponse), tagged by kind:
kind |
Body | Status |
|---|---|---|
result |
DiffResult |
Implemented (always returned) |
job |
DiffJob |
Specified for large diffs, not emitted |
DiffResult:
| Field | Type | Notes |
|---|---|---|
left |
SnapshotId |
|
right |
SnapshotId |
|
changes |
array of FileChange |
|
patch_bundle_digest |
Digest |
Optional. Set only for format=patch-bundle |
FileChange:
| Field | Type | Notes |
|---|---|---|
path |
string | POSIX-style, relative to the snapshot root |
kind |
enum | See Change kinds |
old_mode |
u32 | Optional. Previous mode |
new_mode |
u32 | Optional. New mode |
size_bytes |
u64 | Optional. New content size |
content_digest |
Digest |
Optional. New content digest (file adds and modifies) |
unified_diff |
string | Optional. Unified text diff |
Workspaces¶
| Method | Path | Status | Spec |
|---|---|---|---|
POST |
/v1/workspaces |
Implemented | §13.6 |
GET |
/v1/workspaces/{id} |
Implemented | §13.6 |
POST |
/v1/workspaces/{id}:fork |
Implemented | §13.7 |
POST |
/v1/workspaces/{id}:issueMountTicket |
Implemented | §13.8 |
POST |
/v1/workspaces/{id}:unmount |
Specified, not served | §13.13 |
POST |
/v1/workspaces/{id}:acquireLease |
Specified, not served | gRPC only |
POST |
/v1/workspaces/{id}:releaseLease |
Specified, not served | gRPC only |
POST |
/v1/workspaces/{id}:revokeLease |
Specified, not served | gRPC only |
The router registers GET and POST on /v1/workspaces/{ws}. A GET with
a verb suffix, or a POST verb other than fork and issueMountTicket,
returns NOT_FOUND. The unmount and lease operations live on the gRPC
services instead (see Current scope).
POST /v1/workspaces¶
Creates a workspace from a READY snapshot. Idempotent. Requires the
CreateWorkspace permission.
Request fields (CreateWorkspaceRequest):
| Field | Type | Required | Notes |
|---|---|---|---|
project_id |
ProjectId |
Yes | |
snapshot_id |
SnapshotId |
Yes | Must be READY and same-tenant |
name |
string | No | |
ttl_seconds |
u64 | Yes | Sets expires_at |
labels |
map | No | Omitted when empty |
A non-READY source snapshot returns SNAPSHOT_NOT_READY.
GET /v1/workspaces/{id}¶
Returns one workspace record. Requires ViewMetadata.
Workspace resource:
| Field | Type | Notes |
|---|---|---|
id |
WorkspaceId |
|
tenant_id |
TenantId |
|
project_id |
ProjectId |
|
base_snapshot_id |
SnapshotId |
|
head_snapshot_id |
SnapshotId |
|
version |
u64 | Optimistic-concurrency version |
state |
WorkspaceState |
See Workspace states |
writer_lease_generation |
u64 | 0 when no lease is held |
expires_at |
timestamp | |
created_by |
string | |
created_at |
timestamp | |
labels |
map | Optional. Omitted when empty |
POST /v1/workspaces/{id}:fork¶
Forks one or more workspaces from the base, head, or a specific retained
snapshot. Idempotent. Requires ForkWorkspace.
Request fields (ForkWorkspaceRequest):
| Field | Type | Required | Notes |
|---|---|---|---|
from |
string | Yes | Wire key is from. Value is base, head, or a SnapshotId |
count |
u32 | Yes | Batch size |
ttl_seconds |
u64 | Yes | Sets expires_at for each child |
name_prefix |
string | No |
The API caps batch size at MAX_FORK_BATCH (1000). Larger fan-out uses
repeated idempotent batches (spec §13.7).
Response (ForkWorkspaceResponse):
| Field | Type |
|---|---|
workspaces |
array of Workspace |
Mount tickets¶
| Method | Path | Status | Spec |
|---|---|---|---|
POST |
/v1/workspaces/{id}:issueMountTicket |
Implemented | §13.8 |
POST /v1/workspaces/{id}:issueMountTicket¶
Mints a short-lived, signed mount ticket. Idempotent. For READ_WRITE the
control plane acquires a single-writer lease and stamps its generation into
the ticket. For READ_ONLY no lease is acquired. Permission maps to the mode:
READ_WRITE needs MountWorkspace; READ_ONLY needs
MountSnapshotReadOnly.
Request fields (IssueMountTicketRequest):
| Field | Type | Required | Notes |
|---|---|---|---|
host_id |
HostId |
Yes | |
mode |
enum | Yes | READ_ONLY, READ_WRITE |
requested_ttl_seconds |
u64 | No | Default 300 seconds |
execution_cgroup |
string | No | For host-side accounting |
An expired workspace (ttl_expires_at <= now) returns WORKSPACE_EXPIRED.
A workspace that already holds a writer lease returns WRITER_LEASE_HELD.
Response (IssueMountTicketResponse):
| Field | Type | Notes |
|---|---|---|
ticket |
MountTicket |
|
mount_session_id |
MountSessionId |
mount_<ulid> |
state |
MountSessionState |
AUTHORIZED |
MountTicket:
| Field | Type | Notes |
|---|---|---|
ticket_id |
string | tkt_<ulid> |
tenant_id |
TenantId |
|
workspace_id |
WorkspaceId |
|
snapshot_id |
SnapshotId |
Head snapshot the mount is sealed against |
root_digest |
Digest |
Sealed root tree digest |
host_id |
HostId |
|
mode |
enum | READ_ONLY, READ_WRITE |
lease_generation |
u64 | Optional. Omitted for READ_ONLY |
target_policy |
TargetPolicy |
|
issued_at |
timestamp | |
expires_at |
timestamp | |
nonce |
string | Single-use, defeats replay |
protocol_version |
u32 | 2 |
signed_ticket |
string | awcmt1_<b64url(payload)>.<b64url(sig)>, Ed25519-signed |
TargetPolicy:
| Field | Type | Notes |
|---|---|---|
allowed_target_roots |
array of string | The host refuses targets outside these roots |
Defaults used by the control plane:
| Constant | Value |
|---|---|
DEFAULT_TICKET_TTL_SECONDS |
300 |
DEFAULT_LEASE_TTL_SECONDS |
90 |
TICKET_PROTOCOL_VERSION |
2 |
Health¶
| Method | Path | Status | Auth |
|---|---|---|---|
GET |
/v1/health |
Implemented | None |
Returns the literal string ok. No bearer token required. Use it as a
liveness probe.
Enums¶
Snapshot states¶
SnapshotState serializes as SCREAMING_SNAKE_CASE (spec §12.1).
| Value | Mountable | Readable |
|---|---|---|
UPLOADING |
No | No |
VALIDATING |
No | No |
READY |
Yes | Yes |
FAILED |
No | No |
DELETING |
No | No |
DELETED |
No | No |
DEGRADED |
No | Yes |
Only READY snapshots may be mounted or forked. DEGRADED snapshots are
readable for reads that tolerate missing objects.
Workspace states¶
WorkspaceState serializes as SCREAMING_SNAKE_CASE (spec §12.2).
| Value |
|---|
CREATING |
FAILED |
IDLE |
MOUNTED |
CHECKPOINTING |
EXPIRING |
DELETED |
Mount session states¶
MountSessionState serializes as SCREAMING_SNAKE_CASE (spec §12.3).
| Value |
|---|
REQUESTED |
AUTHORIZED |
PREPARING |
MOUNTED |
UNMOUNTING |
CLOSED |
FAILED |
CLOSED and FAILED are terminal.
Change kinds¶
ChangeKind serializes as kebab-case (spec §9.12).
| Value | Meaning |
|---|---|
add-file |
A new file was added |
modify-file |
An existing file changed |
delete-path |
A path was deleted |
create-directory |
A new directory was created |
delete-directory |
A directory was deleted |
change-mode |
Only the mode changed |
change-symlink |
A symlink was created or retargeted |
Diff formats¶
DiffFormat serializes as kebab-case (spec §13.14).
| Value | Status |
|---|---|
json |
Implemented (default) |
patch-bundle |
Specified, rejected by the handler |
tar |
Specified, rejected by the handler |
Error model¶
Errors use a stable, machine-readable code and include a request id (spec
§13.1, §13.15). The top-level document is { "error": { ... } }.
ErrorBody:
| Field | Type | Notes |
|---|---|---|
code |
ErrorCode |
Clients branch on this |
message |
string | Human-readable; contains no secrets (spec §16.5) |
request_id |
string | Optional. Omitted when absent |
retryable |
bool | Whether bounded backoff may succeed |
The HTTP status is advisory; the code is authoritative. The mapping below is what the shipped handler emits. For the full code reference, see Error codes.
| Code | HTTP | Retryable |
|---|---|---|
UNAUTHENTICATED |
401 | No |
PERMISSION_DENIED |
403 | No |
HOST_NOT_AUTHORIZED |
403 | No |
NOT_FOUND |
404 | No |
INVALID_ARGUMENT |
422 | No |
OBJECT_MISSING |
422 | No |
OBJECT_CORRUPT |
422 | No |
MOUNT_UNSUPPORTED |
422 | No |
ALREADY_EXISTS |
409 | No |
IDEMPOTENCY_CONFLICT |
409 | No |
SNAPSHOT_NOT_READY |
409 | Yes |
WORKSPACE_BUSY |
409 | Yes |
STALE_LEASE_GENERATION |
409 | No |
WORKSPACE_VERSION_CONFLICT |
409 | No |
QUIESCE_FAILED |
409 | Yes |
WRITER_LEASE_HELD |
409 | Yes |
WORKSPACE_EXPIRED |
410 | No |
RESOURCE_EXHAUSTED |
429 | Yes |
LOCAL_IO_ERROR |
500 | Yes |
CHECKPOINT_PUBLICATION_FAILED |
500 | Yes |
OBJECT_STORE_UNAVAILABLE |
503 | Yes |
CONTROL_PLANE_UNAVAILABLE |
503 | Yes |
Current scope¶
This section records where the shipped build differs from the spec.
Implemented REST endpoints¶
The shipped router serves these routes:
POST /v1/import-sessionsPOST /v1/import-sessions/{id}:batchCheckObjectsPOST /v1/import-sessions/{id}:completeGET /v1/snapshotsGET /v1/snapshots/{id}GET /v1/snapshots/{left}:diff(JSON inline only)POST /v1/workspacesGET /v1/workspaces/{id}POST /v1/workspaces/{id}:forkPOST /v1/workspaces/{id}:issueMountTicketGET /v1/health
Specified but not served as REST¶
The wire contracts for these operations exist, but the router does not register them as REST. The live transport is gRPC (see gRPC services):
POST /v1/workspaces/{id}:unmount(spec §13.13). The shipped path is the host-local gRPC methodUnmountWorkspace.POST /v1/workspaces/{id}:acquireLease,:releaseLease,:revokeLease. The spec does not define REST endpoints for these. AREAD_WRITEmount acquires its writer lease implicitly through:issueMountTicket(spec §13.8). Lease activation, renewal, and release run over the control-plane gRPCLeaseService(ActivateWorkspaceLease,RenewWorkspaceLease,ReleaseWorkspaceLease). Spec §13.10 covers renewal, and §12.4 enumerates the lease states.
Specified but not emitted¶
- Diff formats
patch-bundleandtar(spec §13.14). The handler acceptsformat=jsononly and rejects the others withINVALID_ARGUMENT. - The deferred diff job (
DiffResponse::Job, spec §13.14). The service always returns an inlineDiffResponse::Result. TheDiffJobcontract exists but is not produced. - Batch-check presigned upload URLs (spec §13.4 "MAY"). The contract field
upload_urlsexists. The shipped backends reportmissingonly and omitupload_urls.