openapi: 3.1.0 info: title: Superserve API version: 0.1.0 description: | Superserve provides sandbox infrastructure to run AI agents in the cloud. Powered by Firecracker MicroVMs. ## Sandbox lifecycle ``` active <--> paused --> deleted ``` A sandbox is `active` when running and `paused` after being paused. Resuming a paused sandbox returns it to `active`. Deleting releases all resources. | Endpoint | What it does | |----------|-------------| | `POST /sandboxes` | Create a new sandbox (optionally `from_template`) | | `PATCH /sandboxes/:id` | Partially update a running sandbox (e.g. network rules) | | `POST /sandboxes/:id/pause` | Snapshot full state, suspend the VM | | `POST /sandboxes/:id/resume` | Restore from snapshot, continue where it left off | | `DELETE /sandboxes/:id` | Delete sandbox and all resources | ## Sandbox environment By default sandboxes boot from the curated `superserve/base` template (Ubuntu 24.04, 1 vCPU, 1 GB RAM, 4 GB disk, with Python 3.12, Node.js 22, npm, git, curl, and build-essential pre-installed). Callers can override with any template name (e.g. `superserve/python-3.11`, `superserve/node-22`) or a team-owned template UUID via the `from_template` field on `POST /sandboxes`. ## Files and commands `/files`, `/exec`, and `/exec/stream` run against a single sandbox and use its `X-Access-Token` (returned by create, resume, and activate), not the team API key. Two host forms reach them: - `https://sandbox.superserve.ai/...` with `X-Superserve-Sandbox-Id: `. - `https://boxd-{sandbox_id}.sandbox.superserve.ai/...` — no routing header needed. contact: name: Superserve Team license: name: Proprietary servers: - url: https://api.superserve.ai description: Production paths: /health: get: operationId: health tags: [System] summary: Health check responses: "200": description: Service is healthy content: application/json: schema: type: object properties: status: type: string example: ok version: type: string example: "0.1.0" /billing/pricing/public: get: operationId: getPublicBillingPricing tags: [Billing] summary: Get public PAYG billing pricing description: | Returns the active public pay-as-you-go pricing plan and current resource rates. This endpoint is unauthenticated so public pricing pages can render from the same pricing data used by billing. responses: "200": description: Public pay-as-you-go pricing plan and rates content: application/json: schema: $ref: "#/components/schemas/BillingPricingResponse" "429": $ref: "#/components/responses/TooManyRequests" "503": description: Public pricing is not available content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /billing/summary: get: operationId: getBillingSummary tags: [Billing] summary: Get billing summary description: | Returns team-level financial information for the authenticated team's current billing period. All charge fields are monetary USD values, not raw usage metrics. The team is derived from authentication; callers cannot select a team by query parameter. Access requires `billing:read` and the `tenant_usage_dashboard` rollout gate to be enabled for the team. Phase 1 assumes no pricing tier or rate changes within the current billing period. The summary applies the team's currently active pricing rates to all current-period usage. security: - apiKey: [] responses: "200": description: Current billing period financial summary content: application/json: schema: $ref: "#/components/schemas/BillingSummaryResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "503": description: Pricing is not available for the authenticated team's active plan content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /billing/usage-series: get: operationId: getBillingUsageSeries tags: [Billing] summary: Get timezone-aware billing usage chart buckets description: | Returns zero-filled local-calendar buckets for the half-open [start,end) range. `timezone` must be an IANA timezone name (for example, `America/Chicago`). The response is limited to 400 buckets. Every request requires the authenticated team's `billing:read` permission. `billed_total_usd` is gross CPU and memory usage cost before credits, taxes, or payments; storage `cost_usd` is an informational equivalent while storage remains tracked-only and is excluded from that total. parameters: - name: start in: query required: true description: Absolute range start (inclusive). schema: {type: string, format: date-time} - name: end in: query required: true description: Absolute range end (exclusive); must be after `start`. schema: {type: string, format: date-time} - name: granularity in: query required: true description: Local calendar bucket size. Weeks start Monday at 00:00. schema: {type: string, enum: [hour, day, week, month]} - name: timezone in: query required: true description: IANA timezone used for calendar boundaries and DST rules. schema: {type: string, example: America/Chicago} security: - apiKey: [] responses: "200": description: Usage series content: application/json: schema: $ref: "#/components/schemas/BillingUsageSeriesResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": {$ref: "#/components/responses/Forbidden"} "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "503": description: Pricing is not available for the authenticated team's active plan content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /billing/pricing: get: operationId: getBillingPricing tags: [Billing] summary: Get billing pricing description: | Returns the authenticated team's active pricing plan and current resource rates for billing UI display. The team is derived from authentication; callers cannot select a team by query parameter. Access requires `billing:read` and the `tenant_usage_dashboard` rollout gate to be enabled for the team. security: - apiKey: [] responses: "200": description: Active pricing plan and rates content: application/json: schema: $ref: "#/components/schemas/BillingPricingResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "503": description: Pricing is not available for the authenticated team's active plan content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /teams/{team_id}/billing/usage: parameters: - name: team_id in: path required: true schema: type: string format: uuid get: operationId: getTeamBillingUsage tags: [Billing] summary: Get team billing usage description: | Returns billing usage for the specified team and billing period. Customer callers may only access their own team and must hold `billing:read`. security: - apiKey: [] parameters: - name: period_start in: query required: false schema: type: string format: date-time - name: period_end in: query required: false schema: type: string format: date-time responses: "200": description: Team billing usage for the requested period content: application/json: schema: $ref: "#/components/schemas/TeamBillingUsageResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/InternalError" /teams/{team_id}/billing/periods: parameters: - name: team_id in: path required: true schema: type: string format: uuid get: operationId: listTeamBillingPeriods tags: [Billing] summary: List team billing periods description: | Lists persisted billing periods for the specified team. Customer callers may only access their own team and must hold `billing:read`. security: - apiKey: [] parameters: - name: limit in: query required: false schema: type: integer format: int32 minimum: 1 maximum: 100 responses: "200": description: Billing periods for the team content: application/json: schema: type: object required: [periods] properties: periods: type: array items: $ref: "#/components/schemas/TeamBillingPeriodResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/InternalError" /teams/{team_id}/billing/periods/{period_id}/export-preview: parameters: - name: team_id in: path required: true schema: type: string format: uuid - name: period_id in: path required: true schema: type: string description: | Billing period identifier in `period_start,period_end` RFC3339 form. get: operationId: getTeamBillingExportPreview tags: [Billing] summary: Preview team billing export payload security: - apiKey: [] responses: "200": description: Stripe export preview for the billing period content: application/json: schema: $ref: "#/components/schemas/BillingExportPreviewResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/InternalError" /stripe/webhook: post: operationId: handleStripeWebhook tags: [Billing] summary: Receive Stripe webhooks description: | Stripe webhook receiver. Requests are authenticated with the Stripe signature header instead of an API key. requestBody: required: true content: application/json: schema: type: object additionalProperties: true responses: "200": description: Webhook received "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" /stripe/checkout-session: post: operationId: createStripeCheckoutSession tags: [Billing] summary: Create a Stripe Checkout session security: - apiKey: [] requestBody: required: true content: application/json: schema: type: object required: [success_url, cancel_url] properties: success_url: type: string format: uri cancel_url: type: string format: uri responses: "200": description: Checkout session created content: application/json: schema: $ref: "#/components/schemas/BillingSessionResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/InternalError" /stripe/customer-portal-session: post: operationId: createStripeCustomerPortalSession tags: [Billing] summary: Create a Stripe customer portal session security: - apiKey: [] requestBody: required: true content: application/json: schema: type: object required: [return_url] properties: return_url: type: string format: uri responses: "200": description: Customer portal session created content: application/json: schema: $ref: "#/components/schemas/BillingSessionResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/InternalError" /teams/{team_id}/management: parameters: - name: team_id in: path required: true schema: type: string format: uuid get: operationId: getTeamManagement tags: [RBAC] summary: Get team management state description: | Returns the caller-visible team members, role assignments, and capability flags used by customer-facing member management UI. The authenticated API key determines the actor; customer clients must not send or rely on `X-Actor-User-Id`. security: - apiKey: [] responses: "200": description: Team management state content: application/json: schema: $ref: "#/components/schemas/TeamManagementResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /teams/{team_id}/members: parameters: - name: team_id in: path required: true schema: type: string format: uuid get: operationId: listTeamMembers tags: [RBAC] summary: List team members security: - apiKey: [] responses: "200": description: Team members content: application/json: schema: $ref: "#/components/schemas/TeamMembersResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" post: operationId: addTeamMember tags: [RBAC] summary: Add or invite a team member security: - apiKey: [] requestBody: required: true content: application/json: schema: type: object additionalProperties: false properties: user_id: type: string format: uuid status: type: string enum: [active, invited] required: [user_id] responses: "201": description: Member added content: application/json: schema: $ref: "#/components/schemas/TeamMembershipMutationResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" /teams/{team_id}/members/{user_id}: parameters: - name: team_id in: path required: true schema: type: string format: uuid - name: user_id in: path required: true schema: type: string format: uuid delete: operationId: deactivateTeamMember tags: [RBAC] summary: Deactivate a team member security: - apiKey: [] responses: "204": description: Member deactivated "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" /teams/{team_id}/roles: parameters: - name: team_id in: path required: true schema: type: string format: uuid get: operationId: listTeamRoleAssignments tags: [RBAC] summary: List team role assignments security: - apiKey: [] responses: "200": description: Team role assignments content: application/json: schema: $ref: "#/components/schemas/TeamRoleAssignmentsResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" post: operationId: assignTeamRole tags: [RBAC] summary: Assign a team role security: - apiKey: [] requestBody: required: true content: application/json: schema: type: object additionalProperties: false properties: user_id: type: string format: uuid role_name: type: string required: [user_id, role_name] responses: "201": description: Role assigned content: application/json: schema: $ref: "#/components/schemas/TeamRoleAssignment" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" /teams/{team_id}/roles/{assignment_id}: parameters: - name: team_id in: path required: true schema: type: string format: uuid - name: assignment_id in: path required: true schema: type: string format: uuid delete: operationId: revokeTeamRole tags: [RBAC] summary: Revoke a team role security: - apiKey: [] responses: "204": description: Role revoked "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" /sandboxes: get: operationId: listSandboxes tags: [Sandboxes] summary: List all sandboxes description: | Returns sandboxes belonging to the authenticated team, ordered by creation time (newest first) by default. ## Pagination, sorting, and search Pass `limit` (and `offset`) to fetch one page at a time; the `X-Total-Count` response header reports the total across all pages. Omitting `limit` returns the full list, so existing unpaginated callers are unaffected. Sort with `sort` + `order`, filter by exact `status`, and search names with `q` (case-insensitive substring). ## Filtering by metadata Any query parameter prefixed `metadata.` is treated as a filter clause: `?metadata.env=prod&metadata.owner=agent-7`. Multiple filters AND together — a sandbox matches only if every key/value pair is present in its metadata. Values are compared as exact strings; there is no type coercion or substring matching. security: - apiKey: [] parameters: - name: metadata.{key} in: query required: false description: | Filter sandboxes whose metadata contains an exact `{key}: ` pair. Repeat with different keys to AND multiple filters. Values are always strings. Example: `?metadata.env=prod`. schema: type: string - $ref: "#/components/parameters/ListSearch" - name: status in: query required: false description: Filter by exact sandbox status. Unknown values are a `400`. schema: type: string enum: [starting, active, pausing, paused, resuming, migrating, failed, deleted] - name: sort in: query required: false description: Column to sort by (paired with `order`). schema: type: string enum: [created_at, name, status] default: created_at - $ref: "#/components/parameters/SortOrder" - $ref: "#/components/parameters/PageLimit" - $ref: "#/components/parameters/PageOffset" responses: "200": description: List of sandboxes belonging to the authenticated team headers: X-Total-Count: $ref: "#/components/headers/TotalCount" content: application/json: schema: type: array items: $ref: "#/components/schemas/SandboxListItem" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/InternalError" post: operationId: createSandbox tags: [Sandboxes] summary: Create a new sandbox description: | Creates a sandbox from a template (defaults to `superserve/base` when `from_template` is omitted). When the request returns successfully, the sandbox is ready to use — you can run commands against it immediately. New sandboxes use strict preview routing: only ports explicitly published through `/sandboxes/{sandbox_id}/preview-ports` are reachable. `preview_access` defaults newly published ports to `public` or `private`; each publication may override that default independently. security: - apiKey: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateSandboxRequest" responses: "201": description: Sandbox created content: application/json: schema: $ref: "#/components/schemas/SandboxResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "409": $ref: "#/components/responses/Conflict" "429": description: | Rate-limited or quota reached. The `error.code` distinguishes: - `rate_limited` — request rate exceeded; retry after a short backoff. - `too_many_sandboxes` — team has reached its active sandbox count limit (paused sandboxes do not count; pause or delete a sandbox to free a slot, or contact support to raise the cap). content: application/json: schema: $ref: "#/components/schemas/Error" "503": description: | Temporary create failure. Retry the request after the outage clears. content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /sandboxes/{sandbox_id}: parameters: - $ref: "#/components/parameters/SandboxId" get: operationId: getSandbox tags: [Sandboxes] summary: Get a sandbox by ID security: - apiKey: [] responses: "200": description: Sandbox details content: application/json: schema: $ref: "#/components/schemas/SandboxResponse" "404": $ref: "#/components/responses/NotFound" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" delete: operationId: deleteSandbox tags: [Sandboxes] summary: Delete a sandbox security: - apiKey: [] responses: "204": description: Sandbox deleted "404": $ref: "#/components/responses/NotFound" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" patch: operationId: patchSandbox tags: [Sandboxes] summary: Partially update a running sandbox description: | Applies a partial update to a running sandbox. Each top-level field in the request body is optional; only fields that are present are applied. Omitted top-level fields are left unchanged. Nested objects are full replacements when present — to clear a list, send it as an empty array. At least one top-level field must be present, otherwise the request is rejected with `400`. Unknown top-level fields are also rejected with `400` so typos surface as errors instead of silent no-ops. ## Currently patchable fields - `network` — replaces the egress allow/deny rules. The sandbox must be in the `active` state; patching a paused sandbox returns `409`. Rules take effect immediately and are persisted so they survive a future pause/resume cycle. - `metadata` — replaces the sandbox's metadata tags. Can be updated regardless of sandbox state (active, paused). - `auto_delete_seconds` — sets or clears (`null`) the garbage-collection window for the paused state. Can be updated regardless of sandbox state. When applied to an already-paused sandbox, the deletion deadline counts from the moment of this request — never retroactively from when the sandbox paused — so you always get the full window. - `timeout_seconds` — sets or clears (`null`) the auto-pause timeout. Can be updated regardless of sandbox state; on a paused sandbox it applies to the next active session. The timeout is evaluated against the current active session, so lowering it below already-elapsed time pauses the sandbox promptly. - `preview_access` — sets the default access mode for ports published in the future. Existing per-port modes are unchanged. It also moves an older `legacy_public` sandbox to strict routing. `legacy_public` cannot be selected. security: - apiKey: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SandboxPatch" responses: "204": description: Patch applied "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" /sandboxes/{sandbox_id}/preview-ports: parameters: - $ref: "#/components/parameters/SandboxId" get: operationId: listSandboxPreviewPorts tags: [Sandboxes] summary: List published preview ports description: | Returns the sandbox's preview routing mode and its published ports. For a strict `public` or `private` sandbox, only the ports listed here are published at `https://{port}-{sandbox_id}.`; every other numeric port returns 404. Each row reports its independent access mode. Private ports require a port-scoped token. Clients may send it in a request header named `X-Superserve-Preview-Token`, or use the signed-link query bootstrap described by `PreviewTokenResponse`; unknown ports remain closed. An older `legacy_public` sandbox may still route every listening port until it is updated to a strict mode. security: - apiKey: [] responses: "200": description: Preview routing mode and published-port set content: application/json: schema: $ref: "#/components/schemas/PreviewPortList" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" post: operationId: publishSandboxPreviewPort tags: [Sandboxes] summary: Publish or update a preview port description: | Adds a port to the sandbox's published set, optionally with an explicit `public` or `private` mode. On a new row, omitted `access` inherits the sandbox's current `preview_access` default. On an existing row, omitted `access` preserves that row's current mode; only an explicit value changes it. An explicit private publication is rejected while the sandbox remains `legacy_public`, because legacy routing exposes all ports. security: - apiKey: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PublishPortRequest" responses: "200": description: Published port content: application/json: schema: $ref: "#/components/schemas/PreviewPort" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "500": $ref: "#/components/responses/InternalError" /sandboxes/{sandbox_id}/preview-ports/{port}: parameters: - $ref: "#/components/parameters/SandboxId" - $ref: "#/components/parameters/PreviewPort" delete: operationId: unpublishSandboxPreviewPort tags: [Sandboxes] summary: Unpublish a preview port description: | Removes a port from the published set. On a strict sandbox, the port immediately returns 404 at the edge. The operation is idempotent and retry-safe. Repeating the request, including for a port that is already unpublished, re-synchronizes the host's authoritative published-port set and returns `204`. Retry a `500` to converge after a transient host update failure. A `404` means the sandbox itself does not exist. security: - apiKey: [] responses: "204": description: Port unpublished or already unpublished "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" /sandboxes/{sandbox_id}/preview-ports/{port}/token: parameters: - $ref: "#/components/parameters/SandboxId" - $ref: "#/components/parameters/PreviewPort" post: operationId: mintSandboxPreviewToken tags: [Sandboxes] summary: Mint a token for a private preview port description: | Activates token authentication for an already-published private port and returns a credential scoped to exactly this sandbox, port, and token generation. A machine client sends the response's `token` value in a request header whose name is the response's `header` value. For browser navigation, construct a signed URL by adding the URL-encoded `token` under the query-parameter name returned in `query_param`. An ordinary `GET` exchanges a valid query token for a secure host-only cookie and returns a `302` redirect to the same-origin HTTPS URL with the credential removed. The bootstrap response is `Cache-Control: no-store` and `Referrer-Policy: no-referrer`. Non-`GET` requests and complete WebSocket upgrade handshakes authenticate a valid query token directly because they cannot round-trip that redirect. The edge strips all preview-token header, query, and cookie carriers before forwarding any request to sandbox code. Public or unpublished ports cannot mint credentials. Minting requires sandbox write access; an incapable or stale host is rejected rather than activating a policy it cannot enforce. The request body is optional. Omit it (or send `{}`) for a token that remains valid until rotation, access-mode change, or unpublication. An explicit expiry is also enforced for a cookie bootstrapped from that token, even if the browser still stores the cookie. security: - apiKey: [] requestBody: required: false content: application/json: schema: $ref: "#/components/schemas/PreviewTokenRequest" responses: "200": description: Fresh header and signed-link credential; never cache this response headers: Cache-Control: schema: type: string description: Always `no-store`. content: application/json: schema: $ref: "#/components/schemas/PreviewTokenResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "500": $ref: "#/components/responses/InternalError" /sandboxes/{sandbox_id}/preview-ports/{port}/token/rotate: parameters: - $ref: "#/components/parameters/SandboxId" - $ref: "#/components/parameters/PreviewPort" post: operationId: rotateSandboxPreviewToken tags: [Sandboxes] summary: Rotate a private preview port's token generation description: | Advances only this published private port's token generation, revoking every older token for it while leaving sibling ports unchanged. The revocation is committed even if the current host cannot deliver or enforce the replacement; in that case the request returns an error and no credential. Rotation requires sandbox write access. A successful response contains a fresh non-expiring token for the new generation and identifies both its header and signed-link query carriers. Rotation immediately makes older browser cookies unusable because every request revalidates the cookie's embedded generation. security: - apiKey: [] responses: "200": description: Fresh header and signed-link credential for the new generation; never cache this response headers: Cache-Control: schema: type: string description: Always `no-store`. content: application/json: schema: $ref: "#/components/schemas/PreviewTokenResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "500": $ref: "#/components/responses/InternalError" /sandboxes/{sandbox_id}/pause: parameters: - $ref: "#/components/parameters/SandboxId" post: operationId: pauseSandbox tags: [Sandboxes] summary: Pause a running sandbox description: | Snapshots the sandbox's full state (memory + disk), suspends the VM, and transitions to `paused`. Resume it later to continue exactly where it left off — same memory, same running processes, same files. The request holds until the pause is decided; if the host has not answered by then, the response is `202 Accepted` and the pause continues in the background. Send `Prefer: respond-async` to get `202` as soon as the pause is recorded instead. Either way `GET /sandboxes/{sandbox_id}` reports `paused` once it lands (or `failed` if the VM turned out to be gone). security: - apiKey: [] parameters: - name: Prefer in: header required: false schema: type: string enum: [respond-async] description: > Accept `202` with `{"status":"pausing"}` over a held connection. responses: "204": description: Sandbox paused "202": description: > Pause recorded and in progress; poll the sandbox until it is `paused`. Immediate with `Prefer: respond-async`, otherwise returned when the pause is still in progress after the request's wait. headers: Retry-After: schema: type: integer description: Seconds to wait before polling. content: application/json: schema: type: object required: [status] properties: status: type: string enum: [pausing] "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" /sandboxes/{sandbox_id}/resume: parameters: - $ref: "#/components/parameters/SandboxId" post: operationId: resumeSandbox tags: [Sandboxes] summary: Resume a paused sandbox description: | Restores the sandbox from its paused snapshot. Transitions back to `active` with all state intact — same memory, same processes, same files. security: - apiKey: [] responses: "200": description: Sandbox is now active content: application/json: schema: $ref: "#/components/schemas/ResumeResponse" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "401": $ref: "#/components/responses/Unauthorized" "429": description: "`too_many_sandboxes` — resuming would exceed the team's active sandbox limit; free a slot or contact support to raise it." content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /sandboxes/{sandbox_id}/activate: parameters: - $ref: "#/components/parameters/SandboxId" post: operationId: activateSandbox tags: [Sandboxes] summary: Activate a sandbox description: > Returns the sandbox with a fresh access token. If the sandbox is paused, it is resumed first. Idempotent — calling it on an active sandbox just returns a new token. security: - apiKey: [] responses: "200": description: The sandbox is active. content: application/json: schema: $ref: "#/components/schemas/SandboxResponse" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "401": $ref: "#/components/responses/Unauthorized" "429": description: "`too_many_sandboxes` — auto-resuming a paused sandbox would exceed the active sandbox limit; free a slot or contact support to raise it." content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /sandboxes/{sandbox_id}/secrets: parameters: - $ref: "#/components/parameters/SandboxId" post: operationId: attachSandboxSecret tags: [Sandboxes] summary: Attach a secret to a sandbox description: | Binds a stored secret to an existing sandbox under an env var. The sandbox sees a stand-in token; the credential is swapped in for outbound requests to its allowed hosts. Takes effect for processes started after this call; a paused sandbox applies it on resume. security: - apiKey: [] requestBody: required: true content: application/json: schema: type: object required: [env_key, secret_name] properties: env_key: type: string description: Environment variable the stand-in token is exposed under. secret_name: type: string description: Name of the team secret to bind. responses: "201": description: Secret attached content: application/json: schema: type: object properties: env_key: type: string secret_name: type: string "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" /sandboxes/{sandbox_id}/secrets/{env_key}: parameters: - $ref: "#/components/parameters/SandboxId" - name: env_key in: path required: true schema: type: string description: Environment variable the secret is bound under. delete: operationId: detachSandboxSecret tags: [Sandboxes] summary: Detach a secret from a sandbox description: | Removes a secret binding from an existing sandbox and revokes its stand-in token, so requests that use it are refused — for an already-running process, within about a minute. A paused sandbox applies the change on resume. security: - apiKey: [] responses: "204": description: Secret detached "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" /sandboxes/{sandbox_id}/files: parameters: - $ref: "#/components/parameters/SandboxId" get: operationId: listSandboxFiles tags: [Files] summary: List a directory inside a sandbox description: | Returns a one-level listing of a directory inside the sandbox. The listing is served through the control plane, so it works on every sandbox regardless of when it was created. A paused sandbox is resumed automatically before the listing and stays active afterward. `modified_unix` is 0 for entries on older sandboxes that predate that field. security: - apiKey: [] parameters: - name: path in: query required: false description: Absolute directory path to list. Defaults to "/". schema: type: string example: /home/user responses: "200": description: Directory listing content: application/json: schema: $ref: "#/components/schemas/DirListing" "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "401": $ref: "#/components/responses/Unauthorized" "429": description: "`rate_limited` (retry after backoff) or `too_many_sandboxes` — auto-resuming the paused sandbox would exceed the active sandbox limit; free a slot or contact support to raise it." content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /exec: servers: - url: https://sandbox.superserve.ai description: Shared host — send the sandbox ID in `X-Superserve-Sandbox-Id`. - url: https://boxd-{sandbox_id}.sandbox.superserve.ai description: Per-sandbox host. variables: sandbox_id: default: "" description: The sandbox ID. post: operationId: execRun tags: [Exec] summary: Run a command and wait for it to finish description: | Runs a command to completion and returns its output in a single response, retained up to a server-side cap (see `truncated`). For live or unbounded output use `POST /exec/stream` (Server-Sent Events) or `GET /exec/connect` (WebSocket). A non-zero exit code is returned in the body, not as an HTTP error. The sandbox must be `running`; a paused sandbox returns `503`. Activate it first with `POST /sandboxes/{sandbox_id}/activate` (the SDKs do this automatically). security: - accessToken: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ExecRequest" responses: "200": description: The command finished; output and exit code. content: application/json: schema: $ref: "#/components/schemas/ExecResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailable" /exec/stream: servers: - url: https://sandbox.superserve.ai description: Shared host — send the sandbox ID in `X-Superserve-Sandbox-Id`. - url: https://boxd-{sandbox_id}.sandbox.superserve.ai description: Per-sandbox host. variables: sandbox_id: default: "" description: The sandbox ID. post: operationId: execStream tags: [Exec] summary: Run a command and stream output over SSE description: | Runs a command and streams its output as Server-Sent Events while it runs. Each `data:` line is a JSON object: an output chunk (`{"stdout":"..."}` or `{"stderr":"..."}`), the terminal event (`{"exit_code":N,"finished":true}`), or an error (`{"error":"...","finished":true}`). The stream closes when the command exits. The sandbox must be `running`; a paused sandbox returns `503`. Activate it first with `POST /sandboxes/{sandbox_id}/activate` (the SDKs do this automatically). `timeout_s` is a hard runtime limit: the command is killed after that many seconds regardless of output, exiting with code 124. security: - accessToken: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ExecRequest" responses: "200": description: A Server-Sent Events stream of output and lifecycle events. content: text/event-stream: schema: $ref: "#/components/schemas/ExecStreamEvent" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailable" /exec/connect: servers: - url: https://sandbox.superserve.ai description: Shared host — send the sandbox ID in `X-Superserve-Sandbox-Id`. - url: https://boxd-{sandbox_id}.sandbox.superserve.ai description: Per-sandbox host. variables: sandbox_id: default: "" description: The sandbox ID. get: operationId: execConnect tags: [Exec] summary: Run a command over a WebSocket description: | Runs a command over a WebSocket, streaming output back and accepting stdin over one connection. Connect with two `Sec-WebSocket-Protocol` values: `superserve.exec.v1` and `token.`. I/O rides binary frames prefixed with a one-byte channel, so output is byte-exact; lifecycle and control ride text JSON frames. - First frame (text): an `ExecRequest` JSON object. Omitting `timeout_s` runs with no per-command timeout. - Client frames: a binary frame is stdin, prefixed with channel `0x00` (`[0x00][bytes]`); a text frame is JSON control — `{"type":"stdin_close"}` or `{"type":"signal","name":"SIGINT"}`. Client frames are capped at 4 MiB (servers may enforce as low as 64 KiB during rollout); frames over the cap close the socket with code 1009, so chunk larger stdin. - Server frames: a binary frame is output, prefixed with its channel (`0x01` stdout, `0x02` stderr); a text frame is JSON lifecycle — `{"exit_code":N,"finished":true}`, or `{"error":"...","code":"..."}`. Closes on exit. responses: "101": description: Switching Protocols — the WebSocket is established. /secrets: post: operationId: createSecret tags: [Secrets] summary: Create a secret description: | Stores a credential under the caller's team. The plaintext is envelope-encrypted at rest and is never returned by any API. At sandbox-create time, bind the secret to an environment-variable name via the `secrets` map on `POST /sandboxes`; the agent sees a proxy token in env and the in-host enforcement daemon swaps it for the real value at egress. Use `provider` for built-in shortcuts (e.g. `anthropic`, `openai`, `github`, `stripe`) which auto-fill the auth scheme and allowed upstream hosts. Use `auth` + `hosts` for a custom integration. security: - apiKey: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateSecretRequest" responses: "201": description: Secret created content: application/json: schema: $ref: "#/components/schemas/SecretResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "409": $ref: "#/components/responses/Conflict" "500": $ref: "#/components/responses/InternalError" get: operationId: listSecrets tags: [Secrets] summary: List secrets for the calling team description: | Returns metadata for all secrets owned by the team. Cleartext values are never returned. security: - apiKey: [] responses: "200": description: Secret metadata list content: application/json: schema: type: array items: $ref: "#/components/schemas/SecretResponse" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" /secrets/{name}: parameters: - name: name in: path required: true schema: type: string description: Secret name as set at creation. get: operationId: getSecret tags: [Secrets] summary: Get a secret's metadata description: Returns metadata only — the cleartext value is never returned. security: - apiKey: [] responses: "200": description: Secret metadata content: application/json: schema: $ref: "#/components/schemas/SecretResponse" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" patch: operationId: updateSecretValue tags: [Secrets] summary: Rotate a secret's value description: | Replaces the stored ciphertext with a freshly-encrypted copy of the new value. All sandboxes bound to this secret continue to work; in-host caches are invalidated so the next egress uses the new value. security: - apiKey: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateSecretRequest" responses: "200": description: Secret updated content: application/json: schema: $ref: "#/components/schemas/SecretResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" delete: operationId: deleteSecret tags: [Secrets] summary: Revoke a secret description: | Soft-deletes the secret: the proxy daemon refuses to serve it and new sandbox bindings fail. Existing audit-history queries still resolve the row. The same name can be re-used by creating a new secret. security: - apiKey: [] responses: "204": description: Secret revoked "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" /sandboxes/{sandbox_id}/network: parameters: - $ref: "#/components/parameters/SandboxId" get: operationId: listSandboxNetwork tags: [Sandboxes] summary: List a sandbox's egress activity description: | The unified per-sandbox network log: every outbound connection the sandbox made, merged into one time-ordered stream, most recent first. Each row has a `kind` — `connection` (host, bytes, allow/deny verdict) or `request` (HTTP method, path, status, and the secret used, when a credential was injected). Fields not relevant to a row's kind are omitted. Filter by time window (`since`/`before`) and `verdict`. Paginate by passing the response's `next_cursor` as `before` while `has_more` is true. A `verdict` filter returns only `connection` rows, since request rows carry no verdict. security: - apiKey: [] parameters: - $ref: "#/components/parameters/AuditLimit" - name: before in: query description: >- Pagination cursor or time filter. Pass the previous response's opaque `next_cursor` to page through results, or an RFC3339 timestamp to return rows strictly older than that time. schema: type: string - name: since in: query description: Return rows at or newer than this RFC3339 timestamp. schema: type: string format: date-time - name: verdict in: query description: Filter to connection rows with this verdict. Excludes request rows. schema: type: string enum: [allowed, blocked, failed] responses: "200": description: A page of network events (most recent first) content: application/json: schema: $ref: "#/components/schemas/NetworkEventPage" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" /secrets/{name}/audit: parameters: - $ref: "#/components/parameters/SecretName" get: operationId: listSecretAudit tags: [Secrets] summary: List proxy egress events that used this credential description: | Returns proxy_audit events for every outbound request that the in-host enforcement daemon swapped with this credential, across every sandbox it was bound to. Each row includes the originating sandbox name (null when that sandbox has since been deleted). security: - apiKey: [] parameters: - $ref: "#/components/parameters/AuditLimit" - $ref: "#/components/parameters/AuditBefore" - $ref: "#/components/parameters/AuditStatusFilter" responses: "200": description: Audit events (most recent first) content: application/json: schema: type: array items: $ref: "#/components/schemas/ProxyAuditEvent" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" /secrets/{name}/sandboxes: parameters: - $ref: "#/components/parameters/SecretName" get: operationId: listSecretSandboxes tags: [Secrets] summary: List sandboxes currently bound to this credential description: > Returns the active (non-destroyed) sandboxes that have this credential bound, with the env-var name each binding uses. Useful before rotation or deletion ("which sandboxes will be affected?"). security: - apiKey: [] responses: "200": description: Bound sandboxes content: application/json: schema: type: array items: $ref: "#/components/schemas/SandboxSecretBinding" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" /activity: get: operationId: listActivity tags: [Activity] summary: List team audit-log activity description: | Returns audit-log activity for the authenticated team, ordered by creation time (newest first) by default. Backs the console Audit Logs page. Pass `limit` (and `offset`) to fetch one page at a time; the `X-Total-Count` response header reports the total across all pages. The activity log grows without bound, so omitting `limit` returns only the most recent page (up to the 200-row maximum) — page through older history with `limit` + `offset`. Filter by exact `category` or `status`, restrict to a window with `start`/`end`, and search with `q` (case-insensitive substring across sandbox name, secret name, action, and category). security: - apiKey: [] parameters: - name: category in: query required: false description: > Filter by exact activity category (e.g. `sandbox`, `template`, `secret`, `network`). schema: type: string - name: status in: query required: false description: Filter by exact status. Pass `error` to show only failed events. schema: type: string - name: q in: query required: false description: > Case-insensitive substring match across sandbox name, secret name, action, and category. schema: type: string - name: start in: query required: false description: Only include events at or after this RFC3339 timestamp. schema: type: string format: date-time - name: end in: query required: false description: Only include events at or before this RFC3339 timestamp. schema: type: string format: date-time - name: sort in: query required: false description: Column to sort by (paired with `order`). schema: type: string enum: [created_at] default: created_at - $ref: "#/components/parameters/SortOrder" # Not the shared PageLimit param: the activity log is unbounded, so # omitting `limit` returns only the most recent page (not the full # list, as it does for the bounded sandbox/template lists). - name: limit in: query required: false description: | Maximum rows to return (page size). The activity log is unbounded, so omitting `limit` returns only the most recent page (200 rows) — not the full history. Values above 200 are clamped to 200. schema: type: integer minimum: 1 maximum: 200 default: 200 - $ref: "#/components/parameters/PageOffset" responses: "200": description: Audit-log activity for the authenticated team headers: X-Total-Count: $ref: "#/components/headers/TotalCount" content: application/json: schema: type: array items: $ref: "#/components/schemas/ActivityResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/InternalError" /providers: get: operationId: listProviders tags: [Secrets] summary: List the built-in provider shortcut catalog description: > Returns the providers customers can pass as `provider` on `POST /secrets`. Backend-of-record so the console picker stays in sync as the catalog grows. security: - apiKey: [] responses: "200": description: Provider catalog content: application/json: schema: type: array items: $ref: "#/components/schemas/ProviderShortcut" "401": $ref: "#/components/responses/Unauthorized" /files: servers: - url: https://sandbox.superserve.ai description: Shared host — send the sandbox ID in `X-Superserve-Sandbox-Id`. - url: https://boxd-{sandbox_id}.sandbox.superserve.ai description: Per-sandbox host. variables: sandbox_id: default: "" description: The sandbox ID. parameters: - name: path in: query required: true schema: type: string description: > Absolute path inside the sandbox (e.g. `/home/user/out.txt`). get: operationId: readFile tags: [Files] summary: Read a file from a sandbox description: > Returns the file at `path` as raw bytes. To download a directory, set `format=zip` to receive its contents as a zip archive. security: - accessToken: [] parameters: - name: format in: query required: false description: > Set to `zip` to download the directory at `path` as a zip archive. Omit when downloading a single file. schema: type: string enum: [zip] responses: "200": description: File contents, or a zip archive when `format=zip`. content: application/octet-stream: schema: type: string format: binary application/zip: schema: type: string format: binary "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/FilesInternalError" post: operationId: writeFile tags: [Files] summary: Write a file to a sandbox description: Creates parent directories as needed and overwrites any existing file. security: - accessToken: [] requestBody: required: true content: application/octet-stream: schema: type: string format: binary responses: "200": description: File written. content: application/json: schema: $ref: "#/components/schemas/FileWriteResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "507": $ref: "#/components/responses/StorageFull" "500": $ref: "#/components/responses/FilesInternalError" /templates: get: operationId: listTemplates tags: [Templates] summary: List templates visible to the caller description: | Returns the caller's team's templates plus the curated system templates (available to everyone, identified by the `superserve/` name prefix — e.g. `superserve/base`, `superserve/python-3.11`, `superserve/node-22`), ordered by creation time (newest first) by default. ## Pagination, sorting, and search Pass `limit` (and `offset`) to page; `X-Total-Count` reports the total across all pages. Omitting `limit` returns the full list. Narrow the shelves with `owner`, sort with `sort` + `order`, and search names with `q` (case-insensitive substring). The legacy `name_prefix` prefix filter is still honored for backward compatibility. security: - apiKey: [] parameters: - name: name_prefix in: query required: false description: Prefix match on the template `name` (legacy; prefer `q`). schema: type: string - $ref: "#/components/parameters/ListSearch" - name: owner in: query required: false description: | Which shelf to return: `all` (team + system, the default), `team` (only the caller's own templates), or `system` (only the curated Superserve templates). schema: type: string enum: [all, team, system] default: all - name: sort in: query required: false description: Column to sort by (paired with `order`). schema: type: string enum: [created_at, name, status, size, built_at] default: created_at - $ref: "#/components/parameters/SortOrder" - $ref: "#/components/parameters/PageLimit" - $ref: "#/components/parameters/PageOffset" responses: "200": description: List of templates headers: X-Total-Count: $ref: "#/components/headers/TotalCount" content: application/json: schema: type: array items: $ref: "#/components/schemas/TemplateResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" post: operationId: createTemplate tags: [Templates] summary: Create a template and kick off the first build description: | Creates a template and queues the first build. The response includes both the template id and the build id, so clients can immediately poll `GET /templates/{id}` for overall status or subscribe to `GET /templates/{id}/builds/{build_id}/logs` for live output. Template starts in status `building`. Poll until it reaches `ready` before creating sandboxes from it. On failure the status becomes `failed` and `error_message` is populated. To rebuild an existing template (e.g. after a failure or when the base image updates), use `POST /templates/{id}/builds`. security: - apiKey: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateTemplateRequest" responses: "202": description: Template created; first build queued content: application/json: schema: $ref: "#/components/schemas/CreateTemplateResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "409": $ref: "#/components/responses/Conflict" "429": description: | Quota reached. The `error.code` distinguishes: - `too_many_builds` — team has reached its concurrent build limit; wait for an active build to finish. - `too_many_templates` — team has reached its total template count limit; delete templates or contact support to raise the cap. content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /templates/{template_id}: parameters: - $ref: "#/components/parameters/TemplateId" get: operationId: getTemplate tags: [Templates] summary: Get a template by ID security: - apiKey: [] responses: "200": description: Template details content: application/json: schema: $ref: "#/components/schemas/TemplateResponse" "404": $ref: "#/components/responses/NotFound" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" delete: operationId: deleteTemplate tags: [Templates] summary: Delete a template description: | Delete a template owned by the caller's team. Returns `409` if any active or paused sandbox still references this template, or if a build is currently in progress; cancel those first. security: - apiKey: [] responses: "204": description: Template deleted "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" /templates/{template_id}/builds: parameters: - $ref: "#/components/parameters/TemplateId" get: operationId: listTemplateBuilds tags: [Templates] summary: List recent builds for a template security: - apiKey: [] parameters: - name: limit in: query required: false schema: type: integer default: 20 minimum: 1 maximum: 100 responses: "200": description: List of builds, newest first content: application/json: schema: type: array items: $ref: "#/components/schemas/TemplateBuildResponse" "404": $ref: "#/components/responses/NotFound" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" post: operationId: createTemplateBuild tags: [Templates] summary: Rebuild an existing template description: | Queues a new build for this template. Used to retry after a failed build, or to rebuild when the base image has been updated. The **first** build is queued automatically when the template is created via `POST /templates` — this endpoint is only for subsequent builds. ## Idempotency If an in-flight build (`pending`/`building`/`snapshotting`) already exists for this template with the same build spec, this endpoint returns the existing build's id with `200` instead of creating a duplicate. security: - apiKey: [] responses: "201": description: Build created content: application/json: schema: $ref: "#/components/schemas/TemplateBuildResponse" "200": description: Existing in-flight build returned (idempotent) content: application/json: schema: $ref: "#/components/schemas/TemplateBuildResponse" "404": $ref: "#/components/responses/NotFound" "401": $ref: "#/components/responses/Unauthorized" "409": $ref: "#/components/responses/Conflict" "429": description: > Team has reached the per-team concurrent build limit. Response body uses error code `too_many_builds`. content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /templates/{template_id}/builds/{build_id}: parameters: - $ref: "#/components/parameters/TemplateId" - $ref: "#/components/parameters/BuildId" get: operationId: getTemplateBuild tags: [Templates] summary: Get a build by ID security: - apiKey: [] responses: "200": description: Build details content: application/json: schema: $ref: "#/components/schemas/TemplateBuildResponse" "404": $ref: "#/components/responses/NotFound" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" delete: operationId: cancelTemplateBuild tags: [Templates] summary: Cancel an in-flight build description: | Cancels an in-flight build. No-op for builds already in a terminal state. security: - apiKey: [] responses: "204": description: Build cancelled (or was already terminal) "404": $ref: "#/components/responses/NotFound" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" /templates/{template_id}/builds/{build_id}/logs: parameters: - $ref: "#/components/parameters/TemplateId" - $ref: "#/components/parameters/BuildId" get: operationId: streamTemplateBuildLogs tags: [Templates] summary: Stream build logs via SSE description: | Server-Sent Events stream of the build's stdout/stderr. Connecting replays buffered output from the start of the build, then streams live as it arrives. Closes when the build reaches a terminal state. security: - apiKey: [] responses: "200": description: SSE stream of build log events content: text/event-stream: schema: $ref: "#/components/schemas/BuildLogEvent" "404": $ref: "#/components/responses/NotFound" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" components: securitySchemes: apiKey: type: apiKey in: header name: X-API-Key accessToken: type: apiKey in: header name: X-Access-Token description: > Per-sandbox token returned by create, resume, and activate. Grants access to a single sandbox's files and commands. Distinct from the team `X-API-Key`. parameters: SandboxId: name: sandbox_id in: path required: true schema: $ref: "#/components/schemas/PublicSandboxId" description: The unique identifier of the sandbox. PreviewPort: name: port in: path required: true schema: type: integer minimum: 1024 maximum: 65535 not: enum: [49983] description: Published sandbox port. Port 49983 is reserved for Superserve's sandbox service. TemplateId: name: template_id in: path required: true schema: type: string format: uuid description: The unique identifier of the template. BuildId: name: build_id in: path required: true schema: type: string format: uuid description: The unique identifier of the template build. SecretName: name: name in: path required: true schema: type: string description: Secret name as set at creation. AuditLimit: name: limit in: query required: false schema: type: integer minimum: 1 maximum: 200 default: 50 AuditBefore: name: before in: query required: false schema: type: integer format: int64 description: Return events with `id` strictly less than this cursor. AuditStatusFilter: name: status in: query required: false schema: type: string enum: [2xx, 3xx, 4xx, 5xx, errors] description: Narrow by HTTP status class; `errors` includes 4xx and 5xx. PageLimit: name: limit in: query required: false description: | Maximum rows to return (page size). Omit to return the full list unpaginated — the default, preserved for backward compatibility with callers that page client-side. Values above 200 are clamped to 200. schema: type: integer minimum: 1 maximum: 200 PageOffset: name: offset in: query required: false description: Rows to skip before the page. Combine with `limit` to paginate. schema: type: integer minimum: 0 default: 0 SortOrder: name: order in: query required: false description: Sort direction applied to `sort`. schema: type: string enum: [asc, desc] default: desc ListSearch: name: q in: query required: false description: Case-insensitive substring match on the resource `name`. schema: type: string headers: TotalCount: description: | Total rows matching the query across all pages, ignoring `limit` and `offset`. Read this alongside a `limit`/`offset` request to render pagination controls (page count, "X–Y of Z"). schema: type: integer format: int64 schemas: PublicSandboxId: type: string pattern: '^(sb-[a-z0-9]+-)?[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' description: > Public sandbox ID: a bare UUID, or the region-tagged form `sb--` (e.g. `sb-use-1b4e28ba-…`). Treat as an opaque string; the tagged form routes the request to the sandbox's home region. Endpoints accept both forms interchangeably. BillingSessionResponse: type: object required: [url] properties: id: type: string url: type: string format: uri BillingUsageSeriesResource: type: object description: Usage and backend-priced cost for one resource in a bucket. Usage units are resource-specific (vCPU-seconds, GiB-seconds, or tracked storage GiB-seconds). required: [usage, cost_usd, tracked, billable] properties: usage: type: number format: double cost_usd: type: number format: double tracked: type: boolean billable: type: boolean BillingUsageSeriesBucket: type: object description: A half-open [start,end) absolute interval, clipped at the requested range edges. required: [start, end, cpu, memory, storage, billed_total_usd] properties: start: type: string format: date-time end: type: string format: date-time cpu: $ref: "#/components/schemas/BillingUsageSeriesResource" memory: $ref: "#/components/schemas/BillingUsageSeriesResource" storage: $ref: "#/components/schemas/BillingUsageSeriesResource" billed_total_usd: type: number format: double BillingUsageSeriesResponse: type: object description: Ordered, continuous series of at most 400 timezone-aware buckets. required: [start, end, granularity, timezone, buckets] properties: start: type: string format: date-time end: type: string format: date-time granularity: type: string enum: [hour, day, week, month] timezone: type: string buckets: type: array items: $ref: "#/components/schemas/BillingUsageSeriesBucket" BillingExportPreviewResponse: type: object required: [mode, period_id, team_id, status, items, attempts] properties: mode: type: string enum: [shadow, live] period_id: type: string team_id: type: string format: uuid status: type: string stripe_customer_id: type: string items: type: array items: $ref: "#/components/schemas/BillingExportPreviewItem" attempts: type: array items: $ref: "#/components/schemas/BillingExportAttemptRecord" TeamBillingPeriodResponse: type: object required: [period_id, period_start, period_end, status] properties: period_id: type: string period_start: type: string format: date-time period_end: type: string format: date-time status: type: string blocked_reason: type: string approved_at: type: string format: date-time exported_at: type: string format: date-time finalized_at: type: string format: date-time cancel_at_period_end: type: boolean stripe_customer_id: type: string stripe_subscription_id: type: string stripe_subscription_status: type: string stripe_invoice_status: type: string current_period_start: type: string format: date-time current_period_end: type: string format: date-time TeamBillingUsageResponse: type: object required: - period_id - team_id - status - period_start - period_end - vcpu_seconds - memory_mib_seconds - storage_mib_seconds - cpu_vcpu_hours - memory_gib_hours - storage_gib_hours - resources - updated_at properties: period_id: type: string team_id: type: string format: uuid status: type: string period_start: type: string format: date-time period_end: type: string format: date-time vcpu_seconds: type: number format: double memory_mib_seconds: type: number format: double storage_mib_seconds: type: number format: double cpu_vcpu_hours: type: number format: double memory_gib_hours: type: number format: double storage_gib_hours: type: number format: double resources: type: array items: $ref: "#/components/schemas/BillingUsageResource" resources_by_key: type: object additionalProperties: $ref: "#/components/schemas/BillingUsageResource" exported_at: type: string format: date-time finalized_at: type: string format: date-time updated_at: type: string format: date-time BillingSummaryResponse: type: object required: - mode - billing_mode - permissions - checkout_available - portal_available - payment_setup_required - current_charges_usd - credits_applied_usd - credits_remaining_usd - expected_invoice_amount_usd - cost_breakdown_usd - resources - billing_period - pricing_tier - trial - calculated_at properties: mode: type: string enum: [shadow, live] billing_mode: type: string enum: [shadow, live] permissions: $ref: "#/components/schemas/BillingSummaryPermissions" checkout_available: type: boolean portal_available: type: boolean payment_setup_required: type: boolean current_charges_usd: type: number format: double description: Current period charges before credits. credits_applied_usd: type: number format: double nullable: true description: Credits applied against the current period charges. # Phase 1 exposes float USD values for billing UI display. Invoice-grade # exports should use decimal/cents fields before becoming customer-visible # bills to avoid rounding artifacts. credits_remaining_usd: type: number format: double nullable: true description: Credits still available after applying current period charges. expected_invoice_amount_usd: type: number format: double nullable: true description: Expected invoice amount after credits. stripe_credit_balance_usd: type: number format: double nullable: true description: Stripe-authoritative aggregate remaining credit; null when unavailable. stripe_credits_applied_usd: type: number format: double nullable: true stripe_remaining_credit_usd: type: number format: double nullable: true credit_source: type: string enum: [local_trial, stripe] credit_status: type: string enum: [available, unavailable] credit_observed_at: type: string format: date-time nullable: true cost_breakdown_usd: $ref: "#/components/schemas/BillingSummaryCostBreakdown" resources: type: array items: $ref: "#/components/schemas/BillingSummaryResource" resources_by_key: type: object additionalProperties: $ref: "#/components/schemas/BillingSummaryResource" billing_period: $ref: "#/components/schemas/BillingSummaryPeriod" pricing_tier: $ref: "#/components/schemas/BillingSummaryPricingTier" trial: $ref: "#/components/schemas/BillingTrialBalance" calculated_at: type: string format: date-time description: Timestamp when this live billing summary was calculated. BillingSummaryPermissions: type: object required: [can_view, can_manage] properties: can_view: type: boolean can_manage: type: boolean BillingSummaryCostBreakdown: type: object required: [compute, memory, storage] properties: compute: type: number format: double description: vCPU charges for the current period. memory: type: number format: double description: memory charges for the current period. storage: type: number format: double description: storage charges for the current period. BillingSummaryResource: type: object required: - resource_key - resource - display_name - sort_order - unit - display_unit - usage - tracked - billable - charge_usd properties: resource_key: type: string resource: type: string display_name: type: string sort_order: type: integer unit: type: string display_unit: type: string usage: type: number format: double tracked: type: boolean billable: type: boolean charge_usd: type: number format: double BillingUsageResource: type: object required: - resource_key - resource - display_name - sort_order - unit - display_unit - usage - tracked - billable properties: resource_key: type: string resource: type: string display_name: type: string sort_order: type: integer unit: type: string display_unit: type: string usage: type: number format: double tracked: type: boolean billable: type: boolean BillingSummaryPeriod: type: object required: [start, end] properties: start: type: string format: date-time end: type: string format: date-time BillingSummaryPricingTier: type: object required: [plan_key, plan_name, currency] properties: plan_key: type: string example: payg plan_name: type: string example: Pay-as-you-go currency: type: string example: USD BillingTrialBalance: type: object required: [grant_usd, consumed_usd, remaining_usd, state, eligible] properties: grant_usd: type: number format: double consumed_usd: type: number format: double remaining_usd: type: number format: double state: type: string enum: [no_grant, active, exhausted, expired, ended_by_billing_activation] eligible: type: boolean BillingExportPreviewItem: type: object required: - resource_type - resource_key - display_name - sort_order - display_unit - stripe_event_name - stripe_meter_event_identifier - value properties: resource_type: type: string enum: [cpu, memory, storage] resource_key: type: string display_name: type: string sort_order: type: integer display_unit: type: string stripe_event_name: type: string stripe_meter_event_identifier: type: string value: type: number format: double BillingExportAttemptRecord: type: object required: [id, resource_type, stripe_meter_event_identifier, stripe_event_name, value, status, created_at] properties: id: type: string format: uuid resource_type: type: string enum: [cpu, memory, storage] stripe_meter_event_identifier: type: string stripe_event_name: type: string value: type: number format: double status: type: string enum: [pending, sent, accepted, failed, skipped_shadow,skipped_zero] description: > Export attempt lifecycle. `sent` means Stripe returned a 2xx response. `accepted` is kept for legacy rows and is treated the same as `sent` when reading historical data. error: type: string sent_at: type: string format: date-time created_at: type: string format: date-time CreateSandboxRequest: type: object required: [name] properties: name: type: string minLength: 1 maxLength: 64 description: Human-readable name for the sandbox. from_template: type: string description: > Boot the sandbox from a template. Accepts either a template UUID or a name (e.g. `superserve/base`, `superserve/python-3.11`, `superserve/node-22`, or a team-owned name like `my-python-env`). The template must be owned by the caller's team OR be a curated system template (curated templates use the `superserve/` name prefix). The template's vCPU, memory, and disk values are inherited by the sandbox — they cannot be overridden per-sandbox because the snapshot dictates VM shape. When omitted, defaults to `superserve/base`. timeout_seconds: type: integer format: int32 minimum: 1 maximum: 604800 description: > Optional auto-pause timeout in seconds. The sandbox is paused once its current active session has run this long; each resume starts a fresh window. When unset, the sandbox stays active until explicitly paused. Also settable later via `PATCH /sandboxes/{sandbox_id}`. Maximum 604800 (7 days). auto_delete_seconds: type: integer format: int32 minimum: 0 maximum: 2592000 description: > Optional garbage-collection window for paused sandboxes, in seconds. Once the sandbox has been continuously paused for this long it is deleted automatically. The window arms each time the sandbox pauses and is cancelled by resume, so a sandbox in use is never eligible. `0` deletes the sandbox as soon as it pauses. When unset, paused sandboxes are kept until explicitly deleted. Also settable later via `PATCH /sandboxes/{sandbox_id}`. Maximum 2592000 (30 days). metadata: type: object additionalProperties: type: string description: | Flat string-to-string tags attached to the sandbox at creation. Useful for grouping, owner labels, environment, run IDs, etc. ## Constraints - **Strings only.** Values must be strings. There is no type coercion: `metadata.count=42` filters for the *string* "42". - **At most 64 keys.** - Each key may be at most **256 bytes**. - Each value may be at most **2048 bytes** (2 KB). - The serialized object may be at most **16384 bytes** (16 KB) in total. - Keys starting with `superserve.` or `_superserve` (case- insensitive) are reserved for platform use and rejected. Metadata can be updated after creation via `PATCH /sandboxes/:id`. Filter sandboxes by metadata via the `metadata.{key}` query parameter on `GET /sandboxes`. example: env: prod owner: agent-7 env_vars: type: object additionalProperties: type: string description: | Environment variables injected into every process inside the sandbox (terminal sessions, exec calls). Merged on top of any defaults set by the template's `env` build steps — caller keys win on conflict. Survive pause/resume. example: OPENAI_API_KEY: sk-... DEBUG: "1" run_id: 7f3c-21 network: $ref: "#/components/schemas/NetworkConfig" secrets: type: object additionalProperties: type: string description: | Bind team-stored credentials to environment variables inside the sandbox. Keys are env-var names; values are secret names from `POST /secrets`. The agent never sees the real value: it sees a proxy token in env, and the in-host enforcement daemon swaps the token for the real credential at egress. example: ANTHROPIC_API_KEY: anthropic-prod GITHUB_TOKEN: ghp-readonly preview_access: type: string enum: [public, private] default: public description: | Default access for newly published preview ports. New sandboxes default to `public`; `private` ports stay closed with 401 until preview-token authentication is introduced. Both modes are strict: only explicitly published ports are reachable. `legacy_public` is reserved for sandboxes created before explicit publication and cannot be selected through the API. ActivityResponse: description: One audit-log activity row returned by the activity list endpoint. type: object properties: id: type: string format: uuid sandbox_id: type: string format: uuid nullable: true description: Set on sandbox events; null for events not tied to a sandbox. template_id: type: string format: uuid nullable: true description: Set on template events; null otherwise. actor_id: type: string format: uuid nullable: true description: > The team member who performed the action; null for system-initiated events (e.g. auto-pause, auto-delete). category: type: string action: type: string status: type: string nullable: true sandbox_name: type: string nullable: true secret_id: type: string format: uuid nullable: true description: Set on secret events; null once the secret is purged. secret_name: type: string nullable: true duration_ms: type: integer format: int32 nullable: true error: type: string nullable: true metadata: type: object additionalProperties: true created_at: type: string format: date-time SandboxListItem: description: Sandbox shape returned by list endpoints. type: object properties: id: $ref: "#/components/schemas/PublicSandboxId" name: type: string status: type: string enum: [active, pausing, paused, resuming, migrating, failed] description: > Current state of the sandbox. `active` means running; `paused` means paused and awaiting resume. `pausing`, `resuming` and `migrating` are transient states observed while the platform pauses, restores, or relocates a sandbox (e.g. after a `202` from `/pause`, or auto-resume on `/exec`); clients should poll or retry shortly. `failed` means the sandbox can no longer be resumed. vcpu_count: type: integer memory_mib: type: integer snapshot_id: type: string format: uuid description: ID of the latest snapshot (present after a pause). created_at: type: string format: date-time timeout_seconds: type: integer format: int32 description: > Auto-pause timeout in seconds, if configured. Absent when auto-pause is disabled. auto_delete_seconds: type: integer format: int32 description: > Garbage-collection window for the paused state, if configured. Absent when auto-delete is disabled. auto_delete_at: type: string format: date-time description: > When the sandbox will be deleted. Present only while the sandbox is paused with `auto_delete_seconds` configured. The deadline is armed when the sandbox pauses (or when the setting is applied to an already-paused sandbox) and cleared on resume. network: $ref: "#/components/schemas/NetworkConfig" description: > Current egress allow/deny rules, if any have been configured. Absent when the sandbox uses default network settings. metadata: type: object additionalProperties: type: string description: > User-supplied tags attached at creation. Always present — sandboxes created without metadata return `{}` rather than being absent. example: env: prod owner: agent-7 preview_access: type: string enum: [legacy_public, public, private] description: | Default access for newly published ports. `public` and `private` are strict modes; existing published rows keep their own access. `legacy_public` may be returned for an older sandbox and preserves all-port behavior until updated to a strict mode. SandboxResponse: description: Single-sandbox shape — `SandboxListItem` plus `access_token` and bound secrets. allOf: - $ref: "#/components/schemas/SandboxListItem" - type: object properties: access_token: type: string description: | Per-sandbox access token for data-plane operations (file upload/download, terminal). Pass as the `X-Access-Token` header. secrets: type: array description: > Credentials bound to this sandbox. Each entry maps an env-var name visible to the agent to the secret name it resolves to. `revoked=true` when the underlying secret has been soft-deleted (the env var still holds the now-useless proxy token). items: type: object required: [env_key, secret_name] properties: env_key: type: string secret_name: type: string revoked: type: boolean default: false ResumeResponse: type: object description: Returned by `POST /sandboxes/{id}/resume`. properties: id: $ref: "#/components/schemas/PublicSandboxId" status: type: string enum: [active] access_token: type: string description: Fresh access token; use this instead of the one from create. DirListing: type: object required: [entries] properties: entries: type: array items: $ref: "#/components/schemas/DirEntry" DirEntry: type: object required: [name, is_dir, size, modified_unix] properties: name: type: string description: Entry name (basename, not a full path). is_dir: type: boolean description: True when the entry is a directory. size: type: integer format: int64 description: Size in bytes. modified_unix: type: integer format: int64 description: Modification time (Unix seconds). 0 on older sandboxes that predate this field. ExecRequest: type: object required: [command] properties: command: type: string minLength: 1 description: Command to execute. Wrapped in `/bin/sh -c` unless `args` is provided. args: type: array items: type: string description: If provided, `command` is used as the binary and `args` as arguments (no shell wrapping). env: type: object additionalProperties: type: string description: Environment variables for the command. working_dir: type: string description: Working directory (default `/home/user`). timeout_s: type: integer default: 30 description: Timeout in seconds. ExecResult: type: object description: Output of a command run to completion via `POST /exec`. required: [stdout, stderr, exit_code] properties: stdout: type: string description: Standard output; see `truncated`. stderr: type: string description: Standard error; see `truncated`. exit_code: type: integer description: Process exit code. A non-zero value is returned, not raised. truncated: type: boolean description: > Present and true when combined stdout/stderr exceeded the server-side retention cap (currently 8 MiB) and the tail was dropped; a notice explaining the cut is also appended to `stderr`. Use `POST /exec/stream` for unbounded output. ExecStreamEvent: type: object description: > One Server-Sent Event from `POST /exec/stream`, delivered as a `data:` line carrying this JSON object. Output events carry `stdout` or `stderr` (each with a `timestamp`); the terminal event carries `finished: true` with `exit_code`, or `error` and `code` if the command could not be run. The stream may also include SSE comment keepalive lines (`: keepalive`), which carry no data. properties: timestamp: type: string format: date-time description: RFC 3339 time the event was emitted (output and terminal events). stdout: type: string description: A chunk of standard output. stderr: type: string description: A chunk of standard error. finished: type: boolean description: Present and `true` on the terminal event. exit_code: type: integer description: Process exit code, on the terminal event. error: type: string description: Error message if the command could not be run. code: type: string description: Error code accompanying `error` (e.g. `bad_request`, `exec_failed`). NetworkConfig: type: object description: > Egress network rules for a sandbox. `allow_out` accepts CIDRs (e.g. `8.8.8.8/32`) and domain names (e.g. `api.openai.com`, `*.github.com`). `deny_out` accepts CIDRs only. Private ranges (10/8, 172.16/12, 192.168/16, 127/8, 169.254/16) are always blocked regardless of rules. properties: allow_out: type: array items: type: string description: CIDRs or domains to allow. example: ["api.openai.com", "*.github.com", "8.8.8.8/32"] deny_out: type: array items: type: string description: CIDRs to deny. Use `0.0.0.0/0` to block all traffic not in `allow_out`. example: ["0.0.0.0/0"] SandboxPatch: type: object description: | Partial update body for `PATCH /sandboxes/{sandbox_id}`. Each top-level field is optional; only fields that are present are applied. Omitted fields are left unchanged. Nested objects are full replacements when present — to clear a list, send it as an empty array. At least one top-level field must be set, otherwise the request is rejected with `400`. Unknown fields are also rejected with `400`. properties: network: allOf: - $ref: "#/components/schemas/NetworkConfig" description: | Replace the sandbox's egress rules. The sandbox must be in the `active` state. The provided `allow_out` and `deny_out` lists fully replace whatever was previously configured. metadata: type: object additionalProperties: type: string description: | Replace the sandbox's metadata tags. Fully replaces the existing metadata — omitted keys are removed. Can be patched regardless of sandbox state. Same validation limits as on create (64 keys, 256-byte keys, 2 KB values, 16 KB total). auto_delete_seconds: type: integer format: int32 nullable: true minimum: 0 maximum: 2592000 description: | Set or clear the garbage-collection window for the paused state. Once the sandbox has been continuously paused for this many seconds it is deleted automatically. `0` deletes as soon as the sandbox pauses; `null` disables auto-delete. On an already-paused sandbox the deadline counts from this request, so the sandbox gets the full window. Maximum 2592000 (30 days). timeout_seconds: type: integer format: int32 nullable: true minimum: 1 maximum: 604800 description: | Set or clear the auto-pause timeout. Same semantics as on create; `null` disables auto-pause. Evaluated against the current active session, so lowering it below already-elapsed time pauses the sandbox promptly. Maximum 604800 (7 days). preview_access: type: string enum: [public, private] description: | Set the default access for newly published ports and move a legacy sandbox to strict routing. Existing per-port modes are unchanged. `legacy_public` cannot be selected through the API. example: metadata: env: prod owner: agent-7 PublishPortRequest: type: object required: [port] additionalProperties: false properties: port: type: integer minimum: 1024 maximum: 65535 not: enum: [49983] description: Port to publish. Port 49983 is reserved for Superserve's sandbox service. access: type: string enum: [public, private] description: | Explicit per-port mode. When omitted for a new row, inherits the sandbox preview_access default. When omitted for an existing row, preserves that row's current mode. PreviewPort: type: object required: [port, access, token_version] properties: port: type: integer minimum: 1024 maximum: 65535 not: enum: [49983] description: Published port. Port 49983 is reserved for Superserve's sandbox service. access: type: string enum: [public, private] description: | Independent access mode for this published port. Requests to a private port must present its preview token using the header named by the token-mint response's `header` field, the signed-link query parameter named by `query_param`, or the cookie established by a successful signed-link `GET` bootstrap. token_version: type: integer format: int64 minimum: 1 description: | Durable generation used to scope credentials for this port. Rotation, access changes, and unpublish/re-publish advance it. PreviewPortList: type: object required: [preview_access, ports] properties: preview_access: type: string enum: [legacy_public, public, private] description: | Sandbox default for newly published ports. `public` and `private` enforce the listed set; `legacy_public` preserves pre-publication all-port routing for older sandboxes until they are updated. ports: type: array items: $ref: "#/components/schemas/PreviewPort" PreviewTokenRequest: type: object additionalProperties: false description: | Optional expiry for a preview token, independent of whether it is sent by header, used in a signed link, or stored by the edge in a browser cookie. Omit the body (or send `{}`) for a credential that remains valid until its generation changes. properties: expires_in_seconds: type: integer format: int64 minimum: 1 maximum: 604800 description: Lifetime in whole seconds; omitted means no time expiry. PreviewTokenResponse: type: object description: | A single port-scoped credential with two explicit carriers. Machine clients normally send `token` in the request header named by `header`. For browser navigation, append the URL-encoded token under the parameter named by `query_param`, for example `https://{port}-{sandbox_id}./?{query_param}={token}`. For an ordinary `GET`, the edge validates the signed link, sets the host-only `__Host-superserve_preview_token` cookie with `Secure`, `HttpOnly`, `SameSite=None`, `Partitioned`, `Path=/`, and no `Domain`, then returns a `302` to a same-origin HTTPS URL with every reserved token parameter removed. Unrelated query data is preserved. The redirect is `Cache-Control: no-store` with `Referrer-Policy: no-referrer`. Non-`GET` requests and genuine WebSocket upgrade handshakes accept the query token directly without a redirect. In every case, the reserved header, query parameter, and cookie are removed before the upstream application receives the request. The edge revalidates cookie tokens on every request, so token expiry, generation rotation, access-mode change, or unpublication invalidates an existing browser session immediately; the browser may retain the now-unusable cookie value. required: [token, port, header, query_param, access, preview_access, token_version] properties: token: type: string description: | Secret token scoped to this sandbox, port, and generation. Send it using the request header named by `header`, or URL-encode it under the signed-link query parameter named by `query_param`. port: type: integer minimum: 1024 maximum: 65535 not: enum: [49983] header: type: string example: X-Superserve-Preview-Token description: Request-header name to use when sending `token`. query_param: type: string example: superserve_preview_token description: Query-parameter name to use when constructing a signed link. access: type: string enum: [private] description: The published port's access mode. preview_access: type: string enum: [legacy_public, public, private] description: Sandbox default for newly published ports. token_version: type: integer format: int64 minimum: 1 description: Exact generation embedded in `token`. expires_at: type: string format: date-time description: | Present only when `expires_in_seconds` was supplied. After this instant the token is rejected in every carrier, including an already-established browser cookie. BuildSpec: type: object required: [from] description: Declaration of how to build a template. properties: from: type: string description: > OCI image reference for the base. Examples: `python:3.11`, `node:22-slim`, `ghcr.io/myorg/foo:v1`. Resolved to a digest at build time and recorded for reproducibility. Must be a Linux/amd64 image. Alpine and distroless bases are rejected at validation. example: python:3.11 steps: type: array items: $ref: "#/components/schemas/BuildStep" description: Ordered list of build steps executed inside the build VM. start_cmd: type: string description: > Optional command started after build steps complete. The snapshot captures the running process, so sandboxes restored from this template come up with the process already live. ready_cmd: type: string description: > Optional readiness probe. Polled every 2s after `start_cmd`, until it exits 0 or 10 minutes elapse. Use to wait for a server to bind its port before snapshotting. BuildStep: type: object description: > A single build step. Exactly one of `run`, `env`, `workdir`, or `user` must be set. properties: run: type: string description: Shell command. Wrapped in `/bin/sh -c` inside the build VM. example: pip install -r requirements.txt env: type: object required: [key, value] description: > Sets an environment variable for subsequent build steps AND the template's runtime default. Sandboxes created from this template inherit it; caller-supplied `env_vars` on sandbox create override on conflict. properties: key: type: string value: type: string workdir: type: string description: > Working directory for subsequent build steps AND the template's runtime default cwd. Absolute paths are used as-is; relative paths resolve against the current workdir (base `/` when none has been set). Auto-created and chowned to the current build user. Per-exec `working_dir` overrides at runtime. example: /srv/app user: type: object required: [name] description: > Switches the user subsequent build steps execute as, and sets the template's runtime default exec user. The user is created if it doesn't exist. Per-exec `user` overrides at runtime. properties: name: type: string description: > Linux username. Must start with a lowercase letter or underscore; allowed characters are [a-z0-9_-], max 31 chars. example: appuser sudo: type: boolean default: false description: When true, grants passwordless sudo to the user. CreateTemplateRequest: type: object required: [name, build_spec] properties: name: type: string minLength: 1 maxLength: 128 pattern: "^[a-z0-9]([a-z0-9._/-]*[a-z0-9])?$" description: > Human-readable name, unique per team. Used as the `from_template` value when creating sandboxes. Lowercase letters, digits, and `.` `_` `/` `-` only; must start and end with a letter or digit. Names starting with `superserve/` are reserved for curated system templates and rejected for team-owned templates. example: my-python-env vcpu: type: integer minimum: 1 maximum: 10 default: 1 memory_mib: type: integer minimum: 256 maximum: 20480 default: 1024 disk_mib: type: integer minimum: 1024 maximum: 65536 default: 4096 build_spec: $ref: "#/components/schemas/BuildSpec" CreateTemplateResponse: type: object description: | Returned by `POST /templates` — includes both the new template id and the id of the first build that was queued for it. Clients can immediately open the build-log SSE stream or poll the template for status transitions. properties: id: type: string format: uuid team_id: type: string format: uuid name: type: string status: type: string enum: [building, ready, failed] vcpu: type: integer memory_mib: type: integer disk_mib: type: integer created_at: type: string format: date-time build_id: type: string format: uuid description: ID of the first build. Use it to stream logs or poll status. TemplateResponse: type: object properties: id: type: string format: uuid team_id: type: string format: uuid name: type: string status: type: string enum: [pending, building, ready, failed] vcpu: type: integer memory_mib: type: integer disk_mib: type: integer size_bytes: type: integer format: int64 description: On-disk size of the template's snapshot bundle (present once `ready`). error_message: type: string description: Last build's error message, if status is `failed`. created_at: type: string format: date-time built_at: type: string format: date-time description: When the template most recently transitioned to `ready`. TemplateBuildResponse: type: object properties: id: type: string format: uuid template_id: type: string format: uuid status: type: string enum: [pending, building, snapshotting, ready, failed, cancelled] build_spec_hash: type: string description: Stable hash of the build_spec at submission time. Used for idempotent submits. error_message: type: string description: > Populated when `status = failed`. The message is prefixed with a stable error code the UI / SDK can key on: `image_pull_failed`, `step_failed`, `boot_failed`, `snapshot_failed`, `start_cmd_failed`, `ready_cmd_failed`, or `build_failed` (fallback). example: "step_failed: step 1/2 failed after 3s: exited with code 100" started_at: type: string format: date-time finalized_at: type: string format: date-time created_at: type: string format: date-time BuildLogEvent: type: object description: A single SSE event from the build log stream. properties: timestamp: type: string format: date-time stream: type: string enum: [stdout, stderr, system] description: > `stdout`/`stderr` are forwarded from the build process. `system` is platform-emitted status text (step boundaries, snapshot phase, terminal status). text: type: string finished: type: boolean description: True on the final event. status: type: string enum: [ready, failed, cancelled] description: Terminal status, present on the final event. FileWriteResult: type: object description: Result of a successful file write. properties: path: type: string description: Absolute path the file was written to inside the sandbox. size: type: integer format: int64 description: Number of bytes written. example: path: /home/user/out.txt size: 1024 CreateSecretRequest: type: object required: [name, value] properties: name: type: string maxLength: 128 pattern: "^[A-Za-z_][A-Za-z0-9_-]*$" description: > Identifier used to reference the secret later (e.g. in the `secrets` map on `POST /sandboxes`). value: type: string maxLength: 8192 description: Cleartext credential. Encrypted at rest; never returned. provider: type: string description: > Built-in provider shortcut (e.g. `anthropic`, `openai`, `github`, `stripe`). When set, auto-fills auth scheme and allowed upstream hosts. Mutually exclusive with `auth` and `hosts`. `github` emits a `per_host` config so the same PAT works for both api.github.com REST (`Bearer`) and github.com git over HTTPS (`Basic` with `x-access-token`). auth: $ref: "#/components/schemas/SecretAuthConfig" hosts: type: array items: type: string maxItems: 16 description: > Upstream allow list. Required when `auth` is set. Each entry is a hostname or single-level wildcard (e.g. `api.example.com`, `*.example.com`). UpdateSecretRequest: type: object required: [value] properties: value: type: string maxLength: 8192 description: New cleartext value to encrypt and store. SecretAuthConfig: description: | Egress auth shape. Use the single-rule form (`type` + type-specific fields) for credentials that authenticate the same way on every host. Use `per_host` when the same credential needs different auth schemes on different hosts of the same provider — for example one host accepts `Bearer` while another accepts `Basic` with a fixed username. Single-rule and `per_host` are mutually exclusive. oneOf: - $ref: "#/components/schemas/SecretAuthConfigSingleRule" - $ref: "#/components/schemas/SecretAuthConfigPerHost" SecretAuthConfigSingleRule: type: object required: [type] properties: type: type: string enum: [bearer, basic, api-key, custom] description: | Egress auth scheme: - `bearer`: `Authorization: Bearer ` - `basic`: `Authorization: Basic ` - `api-key`: custom header (set `header`, optionally `prefix`) - `custom`: caller-defined `headers` map header: type: string description: Header name (required for `api-key`). prefix: type: string description: Optional value prefix (used by `api-key`). username: type: string description: > Optional username for `basic`. When set, this username is used verbatim in the outbound Basic header (overrides any username sent by the client). When empty, the inbound username is preserved, or `x` is used if absent. headers: type: object additionalProperties: type: string maxProperties: 8 description: Header-name → value template (required for `custom`). SecretAuthConfigPerHost: type: object required: [per_host] properties: per_host: type: array minItems: 1 maxItems: 16 items: $ref: "#/components/schemas/SecretPerHostRule" description: > Per-host rule list. The daemon picks the first rule whose `hosts` match the upstream host at egress. Every host referenced here must also appear in the top-level `hosts` allowlist. Rules may not overlap (a host belongs to at most one rule). SecretPerHostRule: type: object required: [hosts, type] properties: hosts: type: array minItems: 1 maxItems: 16 items: type: string description: > Hosts this rule authenticates. Exact hostnames or single-level wildcards (`*.example.com`). type: type: string enum: [bearer, basic, api-key, custom] header: type: string description: Header name (required for `api-key`). prefix: type: string description: Optional value prefix (used by `api-key`). username: type: string description: Username slot for `basic`. Used verbatim in the outbound `Basic ` header. headers: type: object additionalProperties: type: string maxProperties: 8 description: Header-name → value template (required for `custom`). SecretResponse: type: object required: [id, name, auth_type, auth_config, hosts, created_at, updated_at] properties: id: type: string format: uuid name: type: string auth_type: type: string enum: [bearer, basic, api-key, custom, per_host] description: > `per_host` indicates a multi-rule secret; the resolved rules are in `auth_config.per_host`. auth_config: type: object additionalProperties: true description: Resolved auth scheme details (no cleartext value). provider_shortcut: type: string nullable: true description: Provider shortcut used at creation, if any. hosts: type: array items: type: string created_at: type: string format: date-time updated_at: type: string format: date-time last_used_at: type: string format: date-time nullable: true description: Timestamp of the most recent egress that used this secret. ProxyAuditEvent: type: object required: [id, ts, sandbox_id, method, host, path, status] properties: id: type: integer format: int64 description: Monotonic event id — also the pagination cursor. ts: type: string format: date-time sandbox_id: type: string format: uuid sandbox_name: type: string nullable: true description: > Originating sandbox's name at query time. Only populated by cross-sandbox views such as `GET /secrets/{name}/audit`. Null when the sandbox has since been deleted. secret_id: type: string format: uuid description: Secret involved in this request, if any. method: type: string host: type: string description: Upstream host targeted by the request. path: type: string status: type: integer format: int32 description: Status the proxy returned to the sandbox. upstream_status: type: integer format: int32 nullable: true description: Status returned by the upstream (absent if the proxy short-circuited). latency_ms: type: integer format: int32 nullable: true error_code: type: string nullable: true description: Stable error tag when the proxy denied or failed the request. NetworkEventPage: type: object required: [data, next_cursor, has_more] description: A page of network events with pagination metadata. properties: data: type: array items: $ref: "#/components/schemas/NetworkEvent" next_cursor: type: string nullable: true description: >- Opaque pagination cursor. Pass it verbatim as `before` to fetch the next page; do not parse it. Null when has_more is false. has_more: type: boolean description: Whether more rows exist beyond this page. NetworkEvent: type: object required: [kind, id, ts] description: | One row in the unified network log. `kind` selects which fields are present: connection rows carry dst_ip/verdict/bytes, request rows carry method/path/status/secret_id. Unused fields are omitted. properties: kind: type: string enum: [connection, request] id: type: integer format: int64 ts: type: string format: date-time host: type: string description: Destination host (SNI / HTTP Host). dst_ip: type: string description: Connection rows only. dst_port: type: integer format: int32 verdict: type: string enum: [allowed, blocked, failed] description: Connection rows only. match_rule: type: string description: Connection rows — which rule decided (domain, cidr, internal-ip, …). bytes_sent: type: integer format: int64 bytes_recv: type: integer format: int64 method: type: string description: Request rows only. path: type: string status: type: integer format: int32 description: Request rows — HTTP status returned to the sandbox. upstream_status: type: integer format: int32 latency_ms: type: integer format: int32 secret_id: type: string format: uuid description: Request rows — the secret injected, if any. error_code: type: string SandboxSecretBinding: type: object required: [sandbox_id, sandbox_name, env_key, status] properties: sandbox_id: type: string format: uuid sandbox_name: type: string env_key: type: string description: Env-var name the secret resolves to inside the sandbox. status: type: string enum: [active, pausing, paused, resuming, migrating, failed] ProviderShortcut: type: object required: [name, display, auth_type, hosts, token_shape] description: > One entry of the built-in provider catalog. The console renders pickers from this list so a new shortcut added on the backend appears without a frontend redeploy. properties: name: type: string description: Stable identifier used as `provider` on `POST /secrets`. display: type: string description: Human-readable label. auth_type: type: string enum: [bearer, basic, api-key, custom, per_host] auth_config: type: object additionalProperties: true description: Resolved auth shape; same JSON shape as `SecretResponse.auth_config`. hosts: type: array items: type: string token_shape: type: string description: Prefix-shaped sample of the proxy token issued for this provider (e.g. `sk-ant-api03-...`). BillingPricingResponse: type: object required: [plan_key, plan_name, currency, rates] properties: plan_key: type: string description: Active pricing plan key. Authenticated pricing is team-specific; public pricing returns the public PAYG plan. example: payg plan_name: type: string description: Human-readable plan name. example: Pay-as-you-go currency: type: string description: ISO 4217 currency code for all returned rates. example: USD rates: type: array items: $ref: "#/components/schemas/BillingPricingRate" BillingPricingRate: type: object required: [ resource_key, resource, display_name, sort_order, unit, display_unit, price_usd, price_usd_hourly, effective_from, tracked, billable, ] properties: resource_key: type: string description: Stable billing resource key. resource: type: string enum: [memory_gib, storage_gib, vcpu] description: Metered resource name. display_name: type: string description: Human-readable resource name. sort_order: type: integer description: Display ordering for billing surfaces. unit: type: string enum: [second] description: Billing unit for `price_usd`. display_unit: type: string description: Human-readable unit for billing surfaces. price_usd: type: number format: double description: Unit price in USD. example: 0.000014 price_usd_hourly: type: number format: double description: Convenience hourly equivalent for UI display. example: 0.0504 effective_from: type: string format: date-time tracked: type: boolean description: Whether the resource is tracked in usage reporting. billable: type: boolean description: Whether the resource contributes to billing totals. TeamMember: type: object required: [user_id, email, status, created_at, updated_at] properties: user_id: type: string format: uuid email: type: string format: email full_name: type: string nullable: true status: type: string enum: [active, invited, inactive] roles: type: array items: type: string created_at: type: string format: date-time updated_at: type: string format: date-time TeamMembersResponse: type: object required: [members] properties: members: type: array items: $ref: "#/components/schemas/TeamMember" TeamMembershipMutationResponse: type: object required: [team_id, user_id, status, action, platform] properties: team_id: type: string format: uuid user_id: type: string format: uuid status: type: string enum: [active, invited] action: type: string enum: [added] platform: type: boolean TeamManagementResponse: type: object required: [team_id, members, assignments, capabilities] properties: team_id: type: string format: uuid members: type: array items: $ref: "#/components/schemas/TeamMember" assignments: type: array items: $ref: "#/components/schemas/TeamRoleAssignment" capabilities: $ref: "#/components/schemas/TeamManagementCapabilities" mutation_options: $ref: "#/components/schemas/TeamManagementMutationOptions" TeamManagementCapabilities: type: object required: - can_view_role_assignments - can_invite_members - can_deactivate_members - can_assign_roles - can_revoke_roles properties: can_view_role_assignments: type: boolean can_invite_members: type: boolean can_deactivate_members: type: boolean can_assign_roles: type: boolean can_revoke_roles: type: boolean TeamManagementMutationOptions: type: object properties: member_statuses: type: array items: type: string enum: [active, invited] assignable_roles: type: array items: type: string TeamRoleAssignment: type: object required: [assignment_id, user_id, email, role_name, scope_type, team_id, granted_at, created_at, updated_at] properties: assignment_id: type: string format: uuid user_id: type: string format: uuid email: type: string format: email role_name: type: string scope_type: type: string enum: [team] team_id: type: string format: uuid granted_by: type: string format: uuid nullable: true granted_at: type: string format: date-time revoked_at: type: string format: date-time nullable: true created_at: type: string format: date-time updated_at: type: string format: date-time TeamRoleAssignmentsResponse: type: object required: [assignments] properties: assignments: type: array items: $ref: "#/components/schemas/TeamRoleAssignment" Error: type: object description: | Error envelope. `error.code` is a stable, machine-readable identifier (e.g. `bad_request`, `not_found`, `conflict`, `rate_limited`, `too_many_builds`, `too_many_templates`, `too_many_sandboxes`, `image_pull_failed`, `step_failed`, `snapshot_failed`, `start_cmd_failed`, `ready_cmd_failed`, `build_failed`). `error.message` is human-readable and may change between releases; clients should branch on `code`, not `message`. properties: error: type: object properties: code: type: string message: type: string FilesystemError: description: > Error envelope for a failure raised by the sandbox's own filesystem rather than by the platform — typically a network or FUSE mount attached inside the sandbox. `error.code` is `sandbox_filesystem_error`; `errno` names the failure (for example `ESTALE` or `EIO`) and `mount` identifies the mountpoint and filesystem type that served the path. allOf: - $ref: "#/components/schemas/Error" - type: object properties: error: type: object required: [code, errno] properties: code: const: sandbox_filesystem_error errno: type: string path: type: string mount: type: object properties: mountpoint: type: string fstype: type: string responses: BadRequest: description: Invalid request content: application/json: schema: $ref: "#/components/schemas/Error" Unauthorized: description: Missing or invalid API key content: application/json: schema: $ref: "#/components/schemas/Error" Forbidden: description: Caller is authenticated but not allowed to perform the action content: application/json: schema: $ref: "#/components/schemas/Error" NotFound: description: Resource not found content: application/json: schema: $ref: "#/components/schemas/Error" Conflict: description: Operation conflicts with the resource's current state content: application/json: schema: $ref: "#/components/schemas/Error" TooManyRequests: description: > Rate limit hit — the caller's per-team request budget is temporarily exhausted. Response body uses error code `rate_limited`. Retry after a short backoff; the bucket refills continuously. content: application/json: schema: $ref: "#/components/schemas/Error" InternalError: description: Internal server error content: application/json: schema: $ref: "#/components/schemas/Error" StorageFull: description: > The sandbox has run out of disk space. Response body uses error code `sandbox_storage_full`. content: application/json: schema: $ref: "#/components/schemas/Error" FilesInternalError: description: > Internal server error. When `error.code` is `sandbox_filesystem_error`, the sandbox's own filesystem rejected the operation and the body also carries `errno`, `path`, and the `mount` that served it. content: application/json: schema: anyOf: - $ref: "#/components/schemas/Error" - $ref: "#/components/schemas/FilesystemError" ServiceUnavailable: description: > The sandbox is not currently running (for example, paused) or is temporarily unavailable. For a paused sandbox, activate it with `POST /sandboxes/{sandbox_id}/activate` and retry; the SDKs do this automatically. content: application/json: schema: $ref: "#/components/schemas/Error"