openapi: 3.0.3
info:
  title: Prusa Camera Service – Camera API
  version: "1"
  description: |
    REST API for cameras that send their information and snapshots to Prusa Connect.

    * [Quickstart](https://camera-service.prusa3d.com/docs/guide/quickstart)
    * [Authentication and the registration window](https://camera-service.prusa3d.com/docs/guide/authentication)
    * [Camera types](https://camera-service.prusa3d.com/docs/guide/camera-types)
    * [Errors and troubleshooting](https://camera-service.prusa3d.com/docs/guide/errors)

    Every request needs the `Token`, `Fingerprint` and `User-Agent` headers.
    The first call must be made within 15 minutes of creating the camera.
externalDocs:
  description: Camera Service documentation
  url: https://camera-service.prusa3d.com/docs/
servers:
  - url: https://camera-service.prusa3d.com
security:
  - Token: []
    Fingerprint: []
tags:
  - name: Camera
paths:
  /v1/camera-info:
    get:
      tags: [Camera]
      summary: Get camera information
      operationId: getCameraInfo
      parameters:
        - $ref: "#/components/parameters/UserAgent"
      responses:
        "200": { $ref: "#/components/responses/CameraInfo" }
        "400": { $ref: "#/components/responses/MissingUserAgent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationError" }
        "500": { $ref: "#/components/responses/ServerError" }
    put:
      tags: [Camera]
      summary: Send camera information
      operationId: putCameraInfo
      description: |
        Call at boot and whenever the configuration changes. `config` is replaced as a whole.
        Unknown keys are rejected. The response has the same shape as the request.
      parameters:
        - $ref: "#/components/parameters/UserAgent"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CameraInfoRequest" }
            example:
              config:
                name: Workshop camera
                trigger_scheme: THIRTY_SEC
                rotation: 0
                resolution: { width: 1600, height: 1200 }
              features: [snapshot]
              device:
                type: ESP32
                manufacturer: AI-Thinker
                model: ESP32-CAM
                firmware_version: "1.2.0"
                hardware_version: ESP32-CAM rev1
      responses:
        "200": { $ref: "#/components/responses/CameraInfo" }
        "400": { $ref: "#/components/responses/MissingUserAgent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationError" }
        "500": { $ref: "#/components/responses/ServerError" }
  /v1/snapshot:
    put:
      tags: [Camera]
      summary: Upload a snapshot
      operationId: putSnapshot
      description: "Raw JPEG body, maximum snapshot size 1 MB. The Content-Length header is required. Snapshot uploads are limited to 6 uploads per minute per camera; uploading every 10, 30 or 60 seconds stays within the limit. Only the latest snapshot is kept, so upload periodically while the camera is active. Responses 400, 401, 411, 413 and 415 may come from the upload gateway, without a JSON body, or from the service, with a JSON error body: rely on the status code. 429 and 503 come only from the gateway. On this endpoint the 403 message is `Camera access denied`."
      parameters:
        - $ref: "#/components/parameters/UserAgent"
        - name: Content-Length
          in: header
          required: true
          description: Body size in bytes, 1 to 1000000 (1 MB). Chunked transfer is rejected with 411.
          schema: { type: integer, minimum: 1, maximum: 1000000 }
      requestBody:
        required: true
        content:
          image/jpeg:
            schema: { type: string, format: binary }
          image/jpg:
            schema: { type: string, format: binary }
      responses:
        "200":
          description: Snapshot accepted
          content:
            application/json:
              schema:
                type: object
                properties: { status: { type: string, enum: [OK] } }
                required: [status]
        "400": { description: "Empty body, an invalid Content-Length, or a missing User-Agent." }
        "401": { description: "Fingerprint or Token header missing or empty." }
        "403": { $ref: "#/components/responses/Forbidden" }
        "411": { description: "No Content-Length header, for example chunked transfer. Content-Length is required." }
        "413": { description: "Image too large; the maximum snapshot size is 1 MB. Shrink the image, do not retry the same one." }
        "415": { description: "Content-Type is not image/jpeg or image/jpg." }
        "422": { $ref: "#/components/responses/ValidationError" }
        "429": { description: "Rate limit exceeded. Gateway response, no JSON body. Wait before retrying." }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { description: "Rate limit exceeded or gateway unavailable. Gateway response, no JSON body. Wait before retrying." }
components:
  parameters:
    UserAgent:
      name: User-Agent
      in: header
      required: true
      description: Identifies your camera firmware and version, for example `MyCam/1.2.0`. A request without it is rejected with 400.
      schema: { type: string, minLength: 1, example: MyCam/1.2.0 }
  securitySchemes:
    Token:
      type: apiKey
      in: header
      name: Token
      description: 20-character camera token from Prusa Connect or the Prusa app.
    Fingerprint:
      type: apiKey
      in: header
      name: Fingerprint
      description: 16–64 characters, generated by the camera, stable per device. Bound to the token on the first call. A fingerprint longer than 64 characters is currently rejected with a server error (500) on the first call.
  schemas:
    CameraToken: { type: string, minLength: 20, maxLength: 20, pattern: "^[A-Za-z0-9]+$", example: T0nSPU2v05v0pJeKYFYV }
    CameraFingerprint: { type: string, minLength: 16, maxLength: 64, example: 3f9a1c0e7b2d4a6f }
    CameraType:
      type: string
      enum: [Buddy3D, ESP32, WEB, LINK, INTERNAL]
      description: "See [camera types](https://camera-service.prusa3d.com/docs/guide/camera-types)."
    TriggerScheme:
      type: string
      enum: [MANUAL, EACH_LAYER, FIFTH_LAYER, GCODE, TEN_SEC, THIRTY_SEC, SIXTY_SEC, TEN_MIN]
      default: THIRTY_SEC
      description: Type of snapshot trigger scheme. Manual, layer, gcode only for LINK cameras.
    Resolution:
      type: object
      additionalProperties: false
      required: [width, height]
      properties: { width: { type: integer, minimum: 1 }, height: { type: integer, minimum: 1 } }
    NetworkInfo:
      type: object
      additionalProperties: false
      properties:
        lan_mac: { type: string }
        lan_ipv4: { type: string }
        lan_ipv6: { type: string }
        wifi_mac: { type: string }
        wifi_ipv4: { type: string }
        wifi_ipv6: { type: string }
        wifi_ssid: { type: string }
    CameraConfig:
      type: object
      additionalProperties: false
      required: [name]
      properties:
        name: { type: string, minLength: 1, maxLength: 32, description: Camera name shown to users. }
        path: { type: string, example: /dev/video0 }
        driver: { type: string, example: uvcvideo }
        trigger_scheme: { $ref: "#/components/schemas/TriggerScheme" }
        rotation: { type: integer, description: "Physical rotation of the camera image in degrees, as mounted. Stored as sent." }
        resolution: { $ref: "#/components/schemas/Resolution" }
        network_info: { $ref: "#/components/schemas/NetworkInfo" }
    CameraConfigResponse:
      type: object
      properties:
        name: { type: string, description: Camera name shown to users. }
        path: { type: string, example: /dev/video0 }
        driver: { type: string, example: uvcvideo }
        trigger_scheme: { $ref: "#/components/schemas/TriggerScheme" }
        rotation: { type: integer, description: "Physical rotation of the camera image in degrees, as mounted. Stored as sent." }
        resolution: { $ref: "#/components/schemas/Resolution" }
        network_info: { $ref: "#/components/schemas/NetworkInfo" }
    CameraDevice:
      type: object
      additionalProperties: false
      required: [type, firmware_version, hardware_version]
      properties:
        type: { $ref: "#/components/schemas/CameraType" }
        manufacturer: { type: string }
        model: { type: string }
        firmware_version: { type: string, minLength: 1, maxLength: 16, description: The camera's real release version. }
        hardware_version: { type: string, minLength: 1, maxLength: 32 }
        protobuf_version: { type: string, minLength: 1, maxLength: 16, description: Only for cameras implementing the Prusa camera protocol. Kept unchanged when omitted. }
    CameraInfoRequest:
      type: object
      additionalProperties: false
      required: [config, device]
      properties:
        config: { $ref: "#/components/schemas/CameraConfig" }
        features: { type: array, items: { type: string }, description: Kept unchanged when omitted. }
        device: { $ref: "#/components/schemas/CameraDevice" }
    CameraDeviceResponse:
      type: object
      required: [type, manufacturer, model, firmware_version, hardware_version, protobuf_version]
      properties:
        type: { type: string, nullable: true, enum: [Buddy3D, ESP32, WEB, LINK, INTERNAL], description: null when the camera has not been classified. }
        manufacturer: { type: string, nullable: true }
        model: { type: string, nullable: true }
        firmware_version: { type: string, nullable: true }
        hardware_version: { type: string, nullable: true }
        protobuf_version: { type: string, nullable: true }
    CameraInfoResponse:
      type: object
      required: [config, features, device]
      properties:
        config: { $ref: "#/components/schemas/CameraConfigResponse" }
        features: { type: array, items: { type: string } }
        device: { $ref: "#/components/schemas/CameraDeviceResponse" }
    ErrorDetail:
      type: object
      properties: { field: { type: string }, message: { type: string } }
    ErrorResponse:
      type: object
      required: [statusCode, message, errorCode, requestId]
      properties:
        statusCode: { type: integer }
        message: { type: string }
        errorCode: { type: string }
        requestId: { type: string, description: Quote this when contacting support. }
        details: { type: array, items: { $ref: "#/components/schemas/ErrorDetail" } }
  responses:
    CameraInfo:
      description: The camera
      content:
        application/json:
          schema: { $ref: "#/components/schemas/CameraInfoResponse" }
    MissingUserAgent:
      description: User-Agent header missing or empty
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
          example: { statusCode: 400, message: Missing User-Agent header in the request, errorCode: BAD_REQUEST, requestId: 8c1f2b7e }
    Unauthorized:
      description: Token or Fingerprint header missing
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
          example: { statusCode: 401, message: "Missing camera 'token' in headers", errorCode: UNAUTHORIZED, requestId: 8c1f2b7e }
    Forbidden:
      description: Unknown token, fingerprint mismatch, or registration window expired (deliberately indistinguishable)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
          examples:
            cameraInfo:
              summary: On camera-info
              value: { statusCode: 403, message: "Invalid camera 'token' or 'fingerprint'", errorCode: FORBIDDEN, requestId: 8c1f2b7e }
            snapshot:
              summary: On snapshot
              value: { statusCode: 403, message: Camera access denied, errorCode: FORBIDDEN, requestId: 8c1f2b7e }
    ValidationError:
      description: The body is invalid, or the token is not 20 characters, or the fingerprint is shorter than 16
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
          examples:
            body:
              summary: Invalid body
              value: { statusCode: 422, message: Validation failed, errorCode: VALIDATION_ERROR, requestId: 8c1f2b7e, details: [{ field: config.trigger_scheme, message: Invalid option }] }
            credentials:
              summary: Invalid token length
              value: { statusCode: 422, message: Validation error, errorCode: VALIDATION_ERROR, requestId: 8c1f2b7e, details: [{ field: token, message: Token must be exactly 20 characters }] }
    ServerError:
      description: "Server error. Retry later with backoff. A fingerprint longer than 64 characters is also rejected with a 500 on the first call: shorten it, do not retry."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
          example: { statusCode: 500, message: Internal server error, errorCode: INTERNAL_SERVER_ERROR, requestId: 8c1f2b7e }
