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

# Build an image

> Build a custom image (owner, admin, member).

Builds a golden image from a Dockerfile or from a base plus setup
commands. The node boots a build VM from the
base image of the asked size (the default size when none is given),
runs the steps with the workspace's egress policy and no host access,
and freezes it with its memory. The build runs in the background:
202 returns the image `building`; follow `GET /v1/images/{id}/logs`.
Every sandbox made from the image has its size and region.
Plan limits (`limits` in GET /v1/images): 409 `limit_reached`.
400 `invalid_image` names the Dockerfile line that cannot be built.
Audited `image.create`.



## OpenAPI

````yaml /api-reference/openapi.yaml post /v1/images
openapi: 3.1.0
info:
  title: Zode API
  version: 0.1.0
  description: Sandboxes for coding agents. Every route takes an API key as a bearer token.
servers:
  - url: https://api.zode.dev
security:
  - bearerAuth: []
tags:
  - name: Sandboxes
  - name: Commands
  - name: Browser and desktop
  - name: Snapshots
  - name: Images
  - name: Environments
  - name: Flight recorder
  - name: Attestation
  - name: Agents
  - name: Proxying
  - name: Webhooks
  - name: OpenTelemetry
  - name: Alerts
  - name: CI
  - name: Usage and billing
  - name: Workspace
  - name: Platform
paths:
  /v1/images:
    post:
      tags:
        - Images
      summary: Build an image
      description: |-
        Build a custom image (owner, admin, member).

        Builds a golden image from a Dockerfile or from a base plus setup
        commands. The node boots a build VM from the
        base image of the asked size (the default size when none is given),
        runs the steps with the workspace's egress policy and no host access,
        and freezes it with its memory. The build runs in the background:
        202 returns the image `building`; follow `GET /v1/images/{id}/logs`.
        Every sandbox made from the image has its size and region.
        Plan limits (`limits` in GET /v1/images): 409 `limit_reached`.
        400 `invalid_image` names the Dockerfile line that cannot be built.
        Audited `image.create`.
      operationId: createImage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateImageRequest'
      responses:
        '202':
          description: Build started
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomImage'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Error'
        '403':
          $ref: '#/components/responses/Error'
        '409':
          $ref: '#/components/responses/Error'
        '501':
          $ref: '#/components/responses/Error'
        '502':
          $ref: '#/components/responses/Error'
        '503':
          $ref: '#/components/responses/Error'
components:
  schemas:
    CreateImageRequest:
      type: object
      additionalProperties: false
      required:
        - name
      description: Exactly one of `dockerfile` and `setup`.
      properties:
        name:
          type: string
          pattern: ^[a-z0-9][a-z0-9._-]{0,62}$
        dockerfile:
          type: string
          maxLength: 65536
          description: |
            FROM zode/base (or ubuntu:24.04) runs each instruction in the VM;
            any other FROM is built with docker build inside the VM. No build
            context: COPY and ADD of local files are refused.
        setup:
          $ref: '#/components/schemas/ImageSetup'
        vcpus:
          type: integer
          enum:
            - 2
            - 4
            - 8
            - 16
          description: |
            With ram_mb, picks one of the sizes in GET /v1/images `images`
            (see CreateSandboxRequest); both left out is the default size,
            small. The image keeps the size: its sandboxes have it.
        ram_mb:
          type: integer
          enum:
            - 4096
            - 8192
            - 16384
            - 32768
        region:
          type: string
          enum:
            - eu-central
          default: eu-central
          description: '`eu-central` is the only region today; more will be added.'
        timeout_minutes:
          type: integer
          minimum: 1
          maximum: 60
          default: 30
    CustomImage:
      type: object
      required:
        - id
        - name
        - status
        - error
        - source_kind
        - vcpus
        - ram_mb
        - disk_gb
        - base
        - region
        - log_bytes
        - created_by
        - created_at
        - finished_at
      properties:
        id:
          type: string
          pattern: ^img_[a-z2-7]{16}$
        name:
          type: string
        status:
          type: string
          enum:
            - building
            - ready
            - failed
        error:
          type:
            - string
            - 'null'
        source_kind:
          type: string
          enum:
            - dockerfile
            - setup
        vcpus:
          type: integer
        ram_mb:
          type: integer
        disk_gb:
          type: integer
        base:
          type:
            - string
            - 'null'
          description: The base golden it was built from.
        region:
          type: string
        log_bytes:
          type: integer
          format: int64
        created_by:
          oneOf:
            - type: 'null'
            - type: object
              required:
                - member_id
                - email
              properties:
                member_id:
                  type: string
                email:
                  type: string
        created_at:
          type: string
          format: date-time
        finished_at:
          type:
            - string
            - 'null'
          format: date-time
        dockerfile:
          type: string
          description: GET one image and create only.
        setup:
          $ref: '#/components/schemas/ImageSetup'
        warnings:
          type: array
          items:
            type: string
          description: Create only. Instructions that do nothing in Zode.
    ImageSetup:
      type: object
      properties:
        run:
          type: array
          maxItems: 200
          items:
            type: string
          description: Shell commands, run in order with bash.
        env:
          type: object
          additionalProperties:
            type: string
          description: Kept for every shell in the sandbox.
        workdir:
          type: string
          description: Absolute path; created, and the directory of the commands.
        start:
          type: string
          description: >-
            Started before the freeze, so it is already running when a sandbox
            boots.
        ready:
          type: string
          description: Checked every second after start until it exits 0.
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              examples:
                - not_found
                - rate_limited
                - plan_required
              description: |
                Stable machine code; branch on it rather than on `message`.
                Deliberately not a closed enum: new codes are added over time,
                so treat an unknown code by its HTTP status. The codes in use,
                by status (a few appear under two):

                - 400: `bad_request` (malformed JSON or unknown field),
                  `invalid_request`, `bad_signature`, `confirm_required`,
                  `default_environment`, `email_not_allowed`,
                  `endpoint_required`, `invalid_code`, `invalid_email`,
                  `invalid_image`, `invalid_name`, `invalid_nonce`,
                  `invalid_state`, `invalid_token`, `not_admin`,
                  `order_mismatch`, `org_required`, `owner_cannot_leave`,
                  `password_too_long`, `password_too_short`,
                  `personal_workspace`, `same_email`, `secret_unsupported`,
                  `self`, `too_many`, `unknown_channel`,
                  `unknown_environment`, `unknown_installation`,
                  `url_not_allowed`, `use_transfer`
                - 401: `unauthorized`, `invalid_credentials`, `bad_signature`
                - 402: `plan_required` (the plan does not include it),
                  `credit_exhausted` (no credit left and no card on file)
                - 403: `forbidden` (the role may not), `token_scope`, `csrf`,
                  `cors`, `email_unverified`, `installation_not_yours`,
                  `no_user`, `not_yours`, `subscriptions_off`,
                  `wrong_account`, `wrong_password`
                - 404: `not_found`, `code_not_found`, `invalid_invite`,
                  `not_configured`, `unknown_node`, `unknown_org`,
                  `unknown_sandbox`
                - 409: `invalid_state` (wrong sandbox status),
                  `concurrency_limit` (plan's running-sandbox cap),
                  `daily_cap_reached` (workspace daily spend cap),
                  `limit_reached`, `name_taken`, `node_full`, `not_running`,
                  `browser_off`, `desktop_off`, `already_deleted`,
                  `already_linked`, `already_member`, `already_on_plan`,
                  `billing_not_open`, `checkout_required`,
                  `code_not_applicable`, `code_redeemed`, `code_taken`,
                  `endpoint_disabled`, `installation_taken`,
                  `invite_pending`, `last_method`, `no_sandbox`,
                  `not_placed`, `not_waiting`, `sandboxes_running`,
                  `sole_owner`, `too_many_keys`, `trial_unavailable`,
                  `trial_used`
                - 410: `expired`, `used`, `expired_token`, `expired_invite`,
                  `code_expired`, `code_used_up`
                - 413: `too_large`
                - 415: `unsupported_media_type`
                - 429: `rate_limited` (see the RateLimited response),
                  `too_many_viewers` (desktop viewers)
                - 500: `internal`
                - 501 (this server or host cannot do it yet):
                  `feature_unavailable`, `attach_unavailable`,
                  `exec_unavailable`, `snapshots_unavailable`,
                  `images_unavailable`, `recorder_unavailable`,
                  `attestation_unavailable`
                - 502: `node_error`, `host_unreachable`, `key_destroy_failed`,
                  `provider_error`, `github_error`, `recorder_error`
                - 503: `no_capacity`, `unhealthy`, `alerts_unavailable`,
                  `auth_unavailable`, `ci_unavailable`, `email_unavailable`,
                  `ingest_unavailable`, `secrets_unavailable`,
                  `webhook_not_ready`

                The device-flow code and token endpoints answer 400 in the
                RFC 6749/8628 shape instead (OAuthError).
            message:
              type: string
  responses:
    Error:
      description: Error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: zode_tok_<64 hex chars>
      description: >-
        An API key (`zode_tok_...`). Create one in the console under API keys
        (https://console.zode.dev/tokens), or with `zode login`.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.