> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sandywp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Create a sandbox (or restore one from a template)

> Creates a sandbox. All fields are optional; sensible defaults are used (latest WordPress,
PHP 8.3, standard preset, auto-selected worker).

By default creation is **synchronous**: the request waits (up to ~60s) until the sandbox
is ready and returns `status: "ready"` with the admin credentials and login URL. If
provisioning takes longer, the response comes back with `status: "creating"` and HTTP
`202` — poll `GET /api/app/sites/{id}` until it becomes `"ready"`. Pass `background: true`
to skip the wait and return immediately (also `202` while still `"creating"`).

Passing `templateId` restores a saved Template instead of provisioning a blank sandbox
(fast snapshot restore, no reinstall) — free on every plan, subject to the normal
sandbox-count and storage limits. In that mode, only `siteName`, `duration`,
`expirationMode`, and `background` apply; every other field below is silently ignored
(the sandbox inherits the Template's WordPress/PHP stack).




## OpenAPI

````yaml /openapi.yaml post /api/app/sites
openapi: 3.1.0
info:
  title: SandyWP API
  version: 0.1.0
  summary: Complete supported customer API for SandyWP automation.
  description: >
    This is the complete supported customer API for automating SandyWP
    sandboxes, reusable

    Templates and Blueprints, Imports, Workspaces, account credentials, and
    OAuth connection setup.

    It is the stable HTTP contract for developer integrations; browser,
    provider, and operations

    routes on the same host are deliberately not part of this document.


    **Exclusions ledger.** The specification excludes guest/launcher creation
    (`POST /api/sites`),

    account profile/avatar/password/support and GitHub UI handlers, billing
    checkout/portal/sync,

    public launch routes, plugin/theme search proxies, storage, activity
    telemetry, newsletter,

    health, metrics, impersonation, Slack/GitHub callbacks, Git/Polar webhooks,
    all `/api/admin/**`,

    and all `/api/internal/**` worker or SSH-control routes. Those endpoints are
    browser backends,

    provider callbacks, or operational control-plane interfaces rather than
    supported developer APIs.


    ## Conventions


    - Requests and responses are JSON. Send `Content-Type: application/json` on
    writes (the file
      upload and plugin-ZIP-upload endpoints instead use `multipart/form-data`, as noted on those
      operations).
    - Sandboxes and Templates are addressed by their opaque `id` in the API (for
    example
      `site_abc123`, `tpl_xyz789`).
    - Errors use a consistent envelope — see the `Error` schema. Some actions
    require a paid plan
      and fail with HTTP `402` and a specific error code (for example
      `template_sharing_requires_paid_plan`); each is called out on the operation that returns it.
    - **Async by default.** Provisioning and other slow sandbox mutations happen
    on a worker, not
      inline in the HTTP request. An endpoint that starts one either returns a `jobId` to poll (see
      the `Job` / `JobSummary` schemas and each operation's description), returns the resource in a
      non-terminal state (a site `"creating"`, a Template `"building"`) to re-fetch until it settles,
      or both.
    - A personal bearer token can call the authenticated operations below. A
    scoped OAuth token is
      accepted only by `GET /api/account/me` and the Imports family; it needs `import:read` or
      `import:write` for the matching import operation. Other REST operations treat scoped tokens
      as unauthenticated.
  contact:
    name: SandyWP
    url: https://app.sandywp.com/docs/api
servers:
  - url: https://app.sandywp.com
security:
  - bearerAuth: []
tags:
  - name: Templates
    description: >-
      A Template is a snapshot of a finished sandbox — WordPress/PHP versions,
      plugins, themes, content, and database — ready to restore in seconds.
      Templates come from saving a ready sandbox, and can optionally be
      published as a public "launch this demo" link.
  - name: Sites
    description: >-
      A Site is a disposable WordPress sandbox: its lifecycle
      (create/restore/reset/delete) and the per-sandbox tools built on top of it
      (PHP runtime & ini config, WordPress debug settings, database access,
      outgoing-email capture, plugin deploys, the file manager, Git deployment,
      and SSH access).
  - name: Blueprints
    description: >-
      Reviewable WordPress build recipes that can be validated, saved, run on a
      sandbox, or baked into a Template.
  - name: Imports
    description: >-
      Archive-upload workflows for cloning a live WordPress site or pushing a
      local installation. Scoped OAuth tokens may use only this family, with
      `import:read` for reads and `import:write` for mutations.
  - name: Workspaces
    description: Workspace selection plus member and custom-role administration.
  - name: Account
    description: Identity, usage, personal API tokens, and account SSH public keys.
  - name: OAuth
    description: >-
      Public-client registration and PKCE authorization-code token exchange for
      supported connectors.
paths:
  /api/app/sites:
    post:
      tags:
        - Sites
      summary: Create a sandbox (or restore one from a template)
      description: >
        Creates a sandbox. All fields are optional; sensible defaults are used
        (latest WordPress,

        PHP 8.3, standard preset, auto-selected worker).


        By default creation is **synchronous**: the request waits (up to ~60s)
        until the sandbox

        is ready and returns `status: "ready"` with the admin credentials and
        login URL. If

        provisioning takes longer, the response comes back with `status:
        "creating"` and HTTP

        `202` — poll `GET /api/app/sites/{id}` until it becomes `"ready"`. Pass
        `background: true`

        to skip the wait and return immediately (also `202` while still
        `"creating"`).


        Passing `templateId` restores a saved Template instead of provisioning a
        blank sandbox

        (fast snapshot restore, no reinstall) — free on every plan, subject to
        the normal

        sandbox-count and storage limits. In that mode, only `siteName`,
        `duration`,

        `expirationMode`, and `background` apply; every other field below is
        silently ignored

        (the sandbox inherits the Template's WordPress/PHP stack).
      operationId: createSite
      parameters:
        - $ref: '#/components/parameters/WorkspaceHeaderParam'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSiteRequest'
      responses:
        '200':
          description: The sandbox is ready.
          content:
            application/json:
              schema:
                type: object
                required:
                  - site
                properties:
                  site:
                    $ref: '#/components/schemas/Site'
        '202':
          description: >-
            Still provisioning — either the ~60s synchronous wait elapsed, or
            `background: true` was passed. Poll `GET /api/app/sites/{id}` until
            `status` is `"ready"`.
          content:
            application/json:
              schema:
                type: object
                required:
                  - site
                properties:
                  site:
                    $ref: '#/components/schemas/Site'
        '400':
          description: >-
            `invalid_request` — malformed body. `invalid_site_name` — `siteName`
            outside 2–80 characters. `invalid_wordpress_version` /
            `invalid_php_version` / `invalid_provisioning_preset` /
            `invalid_expiration_mode` / `invalid_duration` — value not one of
            the allowed options. `invalid_expiration` — `duration` is not
            allowed for the given `expirationMode` (e.g. `permanent` with
            `inactivity`). `invalid_worker_code` — `workerCode` is not a valid
            worker identifier.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: >-
            `plan_limit_reached` — the workspace is at its active-sandbox limit.
            `storage_limit_reached` — the workspace is at its storage limit.
            `duration_requires_paid_plan` — `duration` is `2w` or `1m` on a free
            plan. `permanent_requires_paid_plan` — `duration` is `permanent` on
            a free plan.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: plan_limit_reached
                  message: Your free plan allows 2 active sandboxes.
                  details:
                    plan: free
                    limit: 2
                    activeSites: 2
        '403':
          description: >-
            `forbidden` — `templateId` refers to a Template owned by a different
            workspace.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: >-
            `template_not_found` — `templateId` does not exist or is not visible
            to the caller. `artifact_not_found` — the Template's snapshot
            artifact is missing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: >-
            `template_not_ready` — `templateId` refers to a Template whose
            `status` is not `"ready"`. `template_no_artifact` — the Template has
            no snapshot artifact yet.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  parameters:
    WorkspaceHeaderParam:
      name: X-SandyWP-Workspace
      in: header
      required: false
      description: >-
        Selects which of the caller's workspaces to act in, for accounts
        belonging to more than one (legacy alias: `X-SandyWP-Organization`).
        Defaults to the caller's own personal workspace when omitted. Every id
        in this API (sandboxes, Templates, repositories) is scoped to a single
        workspace, so this header changes which set of resources is visible.
      schema:
        type: string
  schemas:
    CreateSiteRequest:
      type: object
      description: All fields optional; see individual field descriptions for defaults.
      properties:
        siteName:
          type: string
          minLength: 2
          maxLength: 80
          description: >-
            Display name; the slug is derived from it. A blank/omitted name gets
            a random friendly name, never the caller's account email.
        wordpressVersion:
          $ref: '#/components/schemas/WordPressVersionOption'
        phpVersion:
          $ref: '#/components/schemas/PhpVersionOption'
        workerCode:
          type: string
          pattern: ^[a-zA-Z0-9][a-zA-Z0-9-]{0,31}$
          description: >-
            Pin the sandbox to a specific worker/region (e.g. `eu1`). Omit to
            auto-select.
        provisioningPreset:
          $ref: '#/components/schemas/SiteProvisioningPreset'
        debugOptions:
          $ref: '#/components/schemas/SiteDebugOptions'
          description: >-
            Only takes effect when `provisioningPreset` is `debug` (silently
            ignored, and forced to all-`false`, when the preset is `standard`).
            Unlike `PUT .../debug`, an omitted field here defaults to `true`
            (matching the "debug" preset) rather than `false`.
        duration:
          $ref: '#/components/schemas/SiteDurationOption'
        expirationMode:
          $ref: '#/components/schemas/DemoExpirationMode'
          description: >-
            `fixed` (default), or `inactivity` to restart the selected
            `duration` whenever the sandbox is actively used. A background tab
            does not count as use.
        multisite:
          type: boolean
          description: >-
            Install as a subdirectory WordPress multisite network. Forces cold
            provisioning (bypasses the warm pool) because the network domain is
            baked in at install time. Ignored when `templateId` is set.
        plugins:
          type: array
          items:
            type: string
          maxItems: 10
          description: >-
            wordpress.org plugin slugs to preinstall and activate. Invalid slugs
            are dropped rather than rejected. Ignored when `templateId` is set.
          example:
            - publishpress-authors
            - post-expirator
        themes:
          type: array
          items:
            type: string
          maxItems: 5
          description: >-
            wordpress.org theme slugs to preinstall and activate. Ignored when
            `templateId` is set.
        templateId:
          type: string
          description: >-
            Create by restoring this Template instead of provisioning a blank
            sandbox. When set, only `siteName`, `duration`, `expirationMode`,
            and `background` apply — every other field on this object is
            ignored.
          example: tpl_xyz789
        background:
          type: boolean
          description: >-
            Return immediately with `status: "creating"` instead of waiting (up
            to ~60s) for the sandbox to become ready.
    Site:
      type: object
      description: >-
        The owner-facing view of a sandbox. Strips internal provisioning details
        (container/DB names, password hash, warm-pool ids, routing) and keeps
        only what a caller needs to use and log into the sandbox.
      required:
        - id
        - name
        - slug
        - status
        - publicUrl
        - wordpressVersion
        - phpVersion
        - workerCode
        - adminUsername
        - adminPassword
        - magicLoginUrl
        - permanent
        - expiresAt
        - expirationMode
        - idleTimeoutMinutes
        - createdAt
        - readyAt
        - failureReason
      properties:
        id:
          type: string
          example: site_abc123
        name:
          type: string
          example: my-sandbox
        slug:
          type: string
          example: my-sandbox
        status:
          $ref: '#/components/schemas/SiteStatus'
        publicUrl:
          type: string
          format: uri
          example: https://my-sandbox.sandywp.dev
        wordpressVersion:
          type: string
          description: >-
            The concrete installed version (e.g. `7.0.2`), not the request-time
            alias (e.g. `latest`).
        phpVersion:
          type: string
        workerCode:
          type: string
          example: eu1
        adminUsername:
          type: string
          example: admin
        adminPassword:
          type:
            - string
            - 'null'
          description: >-
            Plaintext admin password. Present here and on single-sandbox
            responses; omitted from list responses (see `SiteListItem`) so it
            never rides along on a bulk poll.
        magicLoginUrl:
          type:
            - string
            - 'null'
          format: uri
        permanent:
          type: boolean
        expiresAt:
          type:
            - string
            - 'null'
          format: date-time
        expirationMode:
          $ref: '#/components/schemas/DemoExpirationMode'
        idleTimeoutMinutes:
          type:
            - integer
            - 'null'
        createdAt:
          type: string
          format: date-time
        readyAt:
          type:
            - string
            - 'null'
          format: date-time
        failureReason:
          type:
            - string
            - 'null'
    Error:
      type: object
      description: >-
        The standard error envelope returned by every endpoint on a non-2xx
        response.
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: >-
                A stable, machine-readable error code (e.g. `invalid_request`,
                `site_not_found`, `plan_limit_reached`). See each operation for
                the codes it can return.
              example: invalid_request
            message:
              type: string
              description: A human-readable message. Safe to display to an end user.
            details:
              type: object
              description: >-
                Optional structured context (e.g. `{"plan": "free", "limit": 2}`
                for a plan-limit error). Omitted entirely on the generic
                internal-error fallback; `{}` when an error has no extra
                context.
              additionalProperties: true
    WordPressVersionOption:
      type: string
      description: >-
        `latest` resolves to the newest stable release. Pre-release entries
        (currently `7.1-beta3`) are replaced by the next beta/RC as they ship,
        and always provision cold (never via the warm pool).
      enum:
        - latest
        - 7.1-beta3
        - 7.0.2
        - '7.0'
        - '6.9'
        - '6.8'
        - '6.7'
        - '6.6'
    PhpVersionOption:
      type: string
      enum:
        - '8.3'
        - '8.2'
        - '8.1'
    SiteProvisioningPreset:
      type: string
      description: >-
        `standard` — no debug settings at provision time. `debug` — provisions
        with WP_DEBUG (and related toggles) on; see `debugOptions`.
      enum:
        - standard
        - debug
    SiteDebugOptions:
      type: object
      description: A sandbox's WordPress debug toggles.
      required:
        - wpDebug
        - wpDebugLog
        - wpDebugDisplay
        - scriptDebug
        - queryMonitor
      properties:
        wpDebug:
          type: boolean
        wpDebugLog:
          type: boolean
        wpDebugDisplay:
          type: boolean
        scriptDebug:
          type: boolean
        queryMonitor:
          type: boolean
    SiteDurationOption:
      type: string
      description: >-
        The sandbox's lifespan, interpreted per `expirationMode`. With
        `expirationMode: fixed`: `1h`, `1d`, `1w` (default), `2w`, `1m`, or
        `permanent`. With `expirationMode: inactivity`: `10m`, `30m`, `1h`, or
        `1d` (there is no inactivity-mode `permanent` — a `400
        invalid_expiration` rejects that combination). `2w`, `1m`, and
        `permanent` additionally require a paid plan (`402
        duration_requires_paid_plan` / `402 permanent_requires_paid_plan`).
      enum:
        - 10m
        - 30m
        - 1h
        - 1d
        - 1w
        - 2w
        - 1m
        - permanent
    DemoExpirationMode:
      type: string
      description: >-
        `fixed` expires a demo/idle-mode sandbox after a fixed lifetime from
        creation/launch; `inactivity` renews the deadline while it is actively
        used.
      enum:
        - fixed
        - inactivity
    SiteStatus:
      type: string
      enum:
        - creating
        - ready
        - expired
        - failed
        - pending_delete
        - deleted
        - cleanup_failed
  responses:
    Unauthorized:
      description: >-
        No valid bearer token was presented, or a scoped OAuth token was used
        outside its allowed Imports and account-identity routes.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: auth_required
              message: A logged-in user is required.
              details: {}
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        Personal API token, sent as `Authorization: Bearer <SANDYWP_API_KEY>`.
        Obtain one from the dashboard account menu (API keys), `sandywp auth
        login`, or `POST /api/account/tokens`. A missing or invalid token
        returns `401 auth_required`. Scoped OAuth tokens are limited to the
        Imports family and `GET /api/account/me`; see the top-level conventions.

````