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

# Configure the template's public launch link

> Manages the Template's public "launch this demo" link at `<origin>/launch/<launchToken>`.
One endpoint, four actions selected by `action`:

- `enable` — turn the link on. Mints a `launchToken` if the Template doesn't already have
  one. Only valid on a `"ready"` Template (`409 template_not_ready` otherwise).
  **Requires a paid plan** (`402 template_sharing_requires_paid_plan`).
- `rotate` — mint a new `launchToken`, invalidating the old URL, and ensure the link is
  enabled. Same `409`/`402` gates as `enable`.
- `disable` — turn the link off. Free, so a downgraded owner can always turn an existing
  link off.
- `update` — change launcher settings (`quotaMode`, `entryMode`, `expirationMode`,
  `expirationMinutes`, `landingPath`, `branding`, `protected`) without touching whether the
  link is enabled. Free, no plan gate.

Templates expose `launchExpirationMode` and `launchExpirationMinutes` on every response; a
`null` minute value means fixed demos default to 24 hours (1440) and inactivity demos
default to 1 hour (60). Both are capped by the server's configured maximum demo lifetime.




## OpenAPI

````yaml /openapi.yaml post /api/app/templates/{id}/launch
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/templates/{id}/launch:
    post:
      tags:
        - Templates
      summary: Configure the template's public launch link
      description: >
        Manages the Template's public "launch this demo" link at
        `<origin>/launch/<launchToken>`.

        One endpoint, four actions selected by `action`:


        - `enable` — turn the link on. Mints a `launchToken` if the Template
        doesn't already have
          one. Only valid on a `"ready"` Template (`409 template_not_ready` otherwise).
          **Requires a paid plan** (`402 template_sharing_requires_paid_plan`).
        - `rotate` — mint a new `launchToken`, invalidating the old URL, and
        ensure the link is
          enabled. Same `409`/`402` gates as `enable`.
        - `disable` — turn the link off. Free, so a downgraded owner can always
        turn an existing
          link off.
        - `update` — change launcher settings (`quotaMode`, `entryMode`,
        `expirationMode`,
          `expirationMinutes`, `landingPath`, `branding`, `protected`) without touching whether the
          link is enabled. Free, no plan gate.

        Templates expose `launchExpirationMode` and `launchExpirationMinutes` on
        every response; a

        `null` minute value means fixed demos default to 24 hours (1440) and
        inactivity demos

        default to 1 hour (60). Both are capped by the server's configured
        maximum demo lifetime.
      operationId: configureTemplateLauncher
      parameters:
        - $ref: '#/components/parameters/TemplateIdParam'
        - $ref: '#/components/parameters/WorkspaceHeaderParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - action
              properties:
                action:
                  type: string
                  enum:
                    - enable
                    - disable
                    - rotate
                    - update
                  description: Required. What to do with the launcher.
                quotaMode:
                  $ref: '#/components/schemas/LaunchQuotaMode'
                  description: >-
                    `update` only. Who a launched demo counts against: `visitor`
                    (default; the visitor's own quota) or `owner` (the Template
                    owner's plan slots).
                entryMode:
                  $ref: '#/components/schemas/LaunchEntryMode'
                  description: >-
                    `update` only. `anonymous` (default; straight to an
                    auto-login demo) or `email` (capture an email first,
                    recorded as a lead — see `GET
                    /api/app/templates/{id}/leads`).
                expirationMode:
                  $ref: '#/components/schemas/DemoExpirationMode'
                  description: >-
                    `update` only. `fixed` expires each demo after a fixed
                    lifetime; `inactivity` renews the deadline while the visitor
                    actively uses WordPress (a background tab, including
                    WordPress heartbeat polling, does not renew it).
                expirationMinutes:
                  type: integer
                  description: >-
                    `update` only. Allowed values depend on `expirationMode` and
                    are NOT a flat enum: fixed lifetimes accept 60, 420, 1440,
                    4320, or 10080 minutes; inactivity lifetimes accept 10, 30,
                    60, or 1440 minutes. Values above the server's configured
                    maximum demo lifetime are rejected regardless of mode.
                    Omitting this while switching `expirationMode` applies that
                    mode's default (1440 for fixed, 60 for inactivity).
                  enum:
                    - 10
                    - 30
                    - 60
                    - 420
                    - 1440
                    - 4320
                    - 10080
                landingPath:
                  type: string
                  description: >-
                    `update` only. Sandbox-local path opened after auto-login,
                    e.g. `/`, `/shop/`, or `/wp-admin/` (default). Must start
                    with `/`, must not start with `//`, and may carry a query
                    string / fragment for deep links into wp-admin.
                  example: /wp-admin/
                branding:
                  description: >-
                    `update` only. Custom copy/colors for the public landing
                    page, or `null` to clear it. Unknown/invalid sub-fields are
                    silently dropped rather than rejected.
                  oneOf:
                    - $ref: '#/components/schemas/LaunchBranding'
                    - type: 'null'
                protected:
                  type: boolean
                  description: >-
                    `update` only. Protected demo, `true` by default. When
                    `true`, every demo launched from this link (after the
                    change) gets `DISALLOW_FILE_MODS` and `DISALLOW_FILE_EDIT`
                    written into its `wp-config.php`, so visitors get wp-admin
                    without the plugin/theme installer, the ZIP uploader, or the
                    file editor.
      responses:
        '200':
          description: The updated Template.
          content:
            application/json:
              schema:
                type: object
                required:
                  - template
                properties:
                  template:
                    $ref: '#/components/schemas/Template'
        '400':
          description: >-
            `invalid_request` — `action` is missing/unrecognized, or `quotaMode`
            / `entryMode` / `expirationMode` / `landingPath` failed its shape
            check. `invalid_demo_expiration` — `expirationMinutes` is not an
            allowed value for the (resulting) `expirationMode` or exceeds the
            server's maximum. `invalid_launch_landing_path` — `landingPath` is
            not a local path.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: >-
            `template_sharing_requires_paid_plan` — `action` is `enable` or
            `rotate` and the workspace is on the free plan.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: template_sharing_requires_paid_plan
                  message: Sharing a template as a public link requires a paid plan.
                  details:
                    plan: free
        '403':
          description: '`forbidden` — the Template belongs to a different workspace.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/TemplateNotFound'
        '409':
          description: >-
            `template_not_ready` — `action` is `enable` or `rotate` and the
            Template's `status` is not `"ready"`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  parameters:
    TemplateIdParam:
      name: id
      in: path
      required: true
      description: The template id.
      schema:
        type: string
      example: tpl_xyz789
    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:
    LaunchQuotaMode:
      type: string
      description: Who a launched demo counts against.
      enum:
        - visitor
        - owner
    LaunchEntryMode:
      type: string
      description: >-
        Whether a launch-link visitor enters anonymously or must give an email
        first.
      enum:
        - anonymous
        - email
    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
    LaunchBranding:
      type: object
      description: >-
        Custom copy/colors for a Template's public launch landing page. Every
        field is optional; unknown or invalid values (bad hex colors,
        non-http(s) URLs, over-length text) are dropped silently rather than
        rejected.
      properties:
        eyebrow:
          type: string
          maxLength: 40
        headline:
          type: string
          maxLength: 90
        subhead:
          type: string
          maxLength: 240
        buttonLabel:
          type: string
          maxLength: 40
        logoUrl:
          type: string
          format: uri
          description: Must be an http(s) URL.
        brandName:
          type: string
          maxLength: 40
        accentColor:
          type: string
          pattern: ^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$
          example: '#3366ff'
        backgroundColor:
          type: string
          pattern: ^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$
        backgroundImageUrl:
          type: string
          format: uri
          description: Must be an http(s) URL.
        hidePoweredBy:
          type: boolean
    Template:
      type: object
      description: A reusable snapshot of a finished sandbox.
      required:
        - id
        - userId
        - orgId
        - createdByUserId
        - name
        - slug
        - description
        - sourceSiteId
        - artifactId
        - status
        - failureReason
        - wordpressVersion
        - phpVersion
        - sizeBytes
        - launchEnabled
        - launchToken
        - launchCount
        - launchQuotaMode
        - launchEntryMode
        - launchExpirationMode
        - launchExpirationMinutes
        - launchLandingPath
        - launchBranding
        - launchProtected
        - sourceBlueprintId
        - sourceBlueprintRevision
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          example: tpl_xyz789
        userId:
          type: string
        orgId:
          type: string
        createdByUserId:
          type:
            - string
            - 'null'
        name:
          type: string
        slug:
          type: string
        description:
          type:
            - string
            - 'null'
        sourceSiteId:
          type:
            - string
            - 'null'
          description: >-
            The sandbox this Template was snapshotted from, if any (null for a
            Blueprint-baked Template).
        artifactId:
          type:
            - string
            - 'null'
          description: >-
            The snapshot artifact backing this Template. Null while `status` is
            `"building"`.
        status:
          $ref: '#/components/schemas/TemplateStatus'
        failureReason:
          type:
            - string
            - 'null'
        wordpressVersion:
          type: string
        phpVersion:
          type: string
        sizeBytes:
          type: integer
        launchEnabled:
          type: boolean
        launchToken:
          type:
            - string
            - 'null'
          description: >-
            Opaque token in the public `<origin>/launch/<launchToken>` URL. Set
            once a link has ever been enabled.
        launchCount:
          type: integer
          description: Number of sandboxes launched from this Template's public link.
        launchQuotaMode:
          $ref: '#/components/schemas/LaunchQuotaMode'
        launchEntryMode:
          $ref: '#/components/schemas/LaunchEntryMode'
        launchExpirationMode:
          $ref: '#/components/schemas/DemoExpirationMode'
        launchExpirationMinutes:
          type:
            - integer
            - 'null'
          description: >-
            Null means the mode default applies: 1440 (24h) for fixed, 60 (1h)
            for inactivity.
        launchLandingPath:
          type: string
          example: /wp-admin/
        launchBranding:
          oneOf:
            - $ref: '#/components/schemas/LaunchBranding'
            - type: 'null'
        launchProtected:
          type: boolean
          description: >-
            Defaults to true. See `protected` on the launcher-configuration
            endpoint.
        sourceBlueprintId:
          type:
            - string
            - 'null'
          description: >-
            Set when this Template was baked from a Blueprint (out of this
            document's scope) rather than snapshotted from a sandbox.
        sourceBlueprintRevision:
          type:
            - integer
            - 'null'
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    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
    TemplateStatus:
      type: string
      enum:
        - building
        - ready
        - 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: {}
    TemplateNotFound:
      description: >-
        No such Template, or it is not visible to the caller. Ownership failures
        are intentionally indistinguishable from a missing Template, so a
        Template id cannot be probed for existence.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: template_not_found
              message: Template not found.
              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.

````