openapi: 3.1.0
info:
  title: SeedVR2 Image Upscale API
  version: 1.0.0
  description: SeedVR2 7B FP16 image upscaling API with synchronous Base64 and OpenAI Images Edits compatibility. Billing mode is selected by the operator and locked when each job is created.
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
servers:
  - url: https://seedvr2.netuser.me
security:
  - bearerAuth: []
paths:
  /api/v1/healthz:
    get:
      operationId: readiness
      security: []
      summary: Service health
      responses:
        "200": {description: Healthy}
        "400": {$ref: "#/components/responses/BadRequest"}
        "503": {$ref: "#/components/responses/Unavailable"}
  /api/v1/livez:
    get:
      operationId: liveness
      security: []
      summary: Process liveness
      responses:
        "200": {description: Process is alive}
        "400": {$ref: "#/components/responses/BadRequest"}
  /api/v1/capabilities:
    get:
      operationId: getCapabilities
      summary: Current model, limits, and price
      responses:
        "200":
          description: Capabilities
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Capabilities"}
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
  /api/v1/uploads:
    post:
      operationId: createUpload
      summary: Create a signed input upload URL
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [filename, content_type]
              properties:
                filename: {type: string, examples: [source.png]}
                content_type: {type: string, enum: [image/png, image/jpeg, image/webp]}
      responses:
        "201":
          description: Signed upload URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  object_key: {type: string}
                  upload_url: {type: string, format: uri}
                  expires_in_seconds: {type: integer}
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
  /api/v1/jobs:
    post:
      operationId: createJob
      summary: Create an image upscale job
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/CreateJob"}
      responses:
        "202":
          description: Job accepted
          headers:
            X-Job-ID: {$ref: "#/components/headers/JobID"}
            X-Idempotent-Replayed: {$ref: "#/components/headers/IdempotentReplayed"}
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Job"}
        "400": {$ref: "#/components/responses/BadRequest"}
        "401": {$ref: "#/components/responses/Unauthorized"}
        "422": {$ref: "#/components/responses/BadRequest"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "503": {$ref: "#/components/responses/Unavailable"}
      parameters:
        - {$ref: "#/components/parameters/IdempotencyKey"}
    get:
      operationId: listJobs
      summary: List jobs
      parameters:
        - {name: status, in: query, schema: {type: string}}
        - {name: limit, in: query, schema: {type: integer, default: 50, maximum: 200}}
        - {name: offset, in: query, schema: {type: integer, default: 0}}
      responses:
        "200":
          description: Paginated jobs
          content:
            application/json:
              schema:
                type: object
                properties:
                  jobs: {type: array, items: {$ref: "#/components/schemas/Job"}}
                  pagination: {$ref: "#/components/schemas/Pagination"}
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
  /api/v1/jobs/{job_id}:
    get:
      operationId: getJob
      summary: Get a job and its temporary download URL when complete
      parameters:
        - {$ref: "#/components/parameters/JobID"}
      responses:
        "200":
          description: Job detail
          headers:
            X-Aihub-Cost-Micro:
              description: Present for succeeded jobs. Actual RMB charge multiplied by 1000000 and rounded to the nearest integer.
              schema: {type: integer, format: int64}
          content:
            application/json:
              schema:
                type: object
                properties:
                  job: {$ref: "#/components/schemas/Job"}
                  download_url: {type: string, format: uri}
                  download_url_expires_in_seconds: {type: integer}
        "404": {$ref: "#/components/responses/NotFound"}
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
  /api/v1/jobs/{job_id}/cancel:
    post:
      operationId: cancelJob
      summary: Cancel a non-terminal job
      parameters:
        - {$ref: "#/components/parameters/JobID"}
      responses:
        "200":
          description: Canceled job
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Job"}
        "409": {$ref: "#/components/responses/BadRequest"}
        "401": {$ref: "#/components/responses/Unauthorized"}
        "429": {$ref: "#/components/responses/RateLimited"}
  /api/v1/images/upscale:
    post:
      operationId: upscaleImageBase64
      summary: Synchronous Base64 image upscale
      description: Accepts a Base64-encoded PNG, JPEG, or WebP image and returns the PNG result as Base64. The size field is the exact final output dimensions.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [image_base64, size]
              properties:
                image_base64:
                  type: string
                  description: Raw Base64 or a data URL. Decoded input limit is 64 MiB.
                size: {type: string, pattern: "^[1-9][0-9]*x[1-9][0-9]*$", examples: [4096x4096]}
                seed: {type: integer, minimum: 0, default: 42}
                color_correction: {type: string, enum: [wavelet, none], default: wavelet}
                resize_method: {type: string, enum: [lanczos, bicubic], default: lanczos}
      responses:
        "200":
          description: Base64 PNG result
          headers:
            X-Job-ID: {$ref: "#/components/headers/JobID"}
            X-Idempotent-Replayed: {$ref: "#/components/headers/IdempotentReplayed"}
            X-Aihub-Cost-Micro:
              description: Actual RMB charge multiplied by 1000000 and rounded. RMB 0.125 is 125000.
              schema: {type: integer, format: int64}
          content:
            application/json:
              schema:
                type: object
                properties:
                  created: {type: integer}
                  width: {type: integer}
                  height: {type: integer}
                  data:
                    type: array
                    minItems: 1
                    maxItems: 1
                    items:
                      type: object
                      required: [b64_json]
                      properties:
                        b64_json: {type: string}
        "400": {$ref: "#/components/responses/BadRequest"}
        "401": {$ref: "#/components/responses/Unauthorized"}
        "422": {$ref: "#/components/responses/BadRequest"}
        "504": {$ref: "#/components/responses/BadRequest"}
        "409": {$ref: "#/components/responses/BadRequest"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "503": {$ref: "#/components/responses/Unavailable"}
      parameters:
        - {$ref: "#/components/parameters/IdempotencyKey"}
  /v1/images/edits:
    post:
      operationId: editImageOpenAICompatible
      summary: OpenAI-compatible synchronous SeedVR2 image upscale
      description: Accepts one input image. The size field is used as the exact final output dimensions. Prompt, model, and quality are accepted for client compatibility but do not change the SeedVR2 upscale workflow.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [image, size]
              properties:
                image: {type: string, format: binary}
                model: {type: string, examples: [seedvr2-7b]}
                prompt: {type: string, description: Accepted for compatibility and audit metadata}
                size: {type: string, pattern: "^[1-9][0-9]*x[1-9][0-9]*$", examples: [4096x4096]}
                n: {type: integer, const: 1, default: 1}
                response_format: {type: string, enum: [b64_json, url], default: b64_json}
                output_format: {type: string, const: png, default: png}
                stream: {type: boolean, const: false, default: false}
                seed: {type: integer, minimum: 0, default: 42}
                color_correction: {type: string, enum: [wavelet, none], default: wavelet}
                resize_method: {type: string, enum: [lanczos, bicubic], default: lanczos}
      responses:
        "200":
          description: OpenAI-compatible image response
          headers:
            X-Job-ID: {$ref: "#/components/headers/JobID"}
            X-Idempotent-Replayed: {$ref: "#/components/headers/IdempotentReplayed"}
            X-Aihub-Cost-Micro:
              description: Actual RMB charge multiplied by 1000000 and rounded to the nearest integer. For example, RMB 0.125 is 125000.
              schema: {type: integer, format: int64}
          content:
            application/json:
              schema:
                type: object
                properties:
                  created: {type: integer}
                  data:
                    type: array
                    minItems: 1
                    maxItems: 1
                    items:
                      type: object
                      properties:
                        b64_json: {type: string}
                        url: {type: string, format: uri}
        "400": {$ref: "#/components/responses/BadRequest"}
        "401": {$ref: "#/components/responses/Unauthorized"}
        "504": {$ref: "#/components/responses/BadRequest"}
        "409": {$ref: "#/components/responses/BadRequest"}
        "429": {$ref: "#/components/responses/RateLimited"}
        "503": {$ref: "#/components/responses/Unavailable"}
      parameters:
        - {$ref: "#/components/parameters/IdempotencyKey"}
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
  parameters:
    JobID:
      name: job_id
      in: path
      required: true
      schema: {type: string}
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Reusing the same key and identical request returns the original job. Reusing it for different input or parameters returns HTTP 409.
      schema: {type: string, maxLength: 128}
  headers:
    JobID:
      description: Internal job ID for support, auditing, and retry reconciliation.
      schema: {type: string}
    IdempotentReplayed:
      description: Present and true when an existing job was returned for an idempotent retry.
      schema: {type: string, const: "true"}
  schemas:
    CreateJob:
      type: object
      required: [input_object_key, input_width, input_height, output_width, output_height]
      properties:
        input_object_key: {type: string}
        input_width: {type: integer, minimum: 1}
        input_height: {type: integer, minimum: 1}
        output_width: {type: integer, minimum: 1}
        output_height: {type: integer, minimum: 1}
        output_content_type: {type: string, const: image/png, default: image/png}
        priority: {type: integer, default: 0}
        parameters:
          type: object
          properties:
            seed: {type: integer, minimum: 0, default: 42}
            color_correction: {type: string, enum: [wavelet, none], default: wavelet}
            resize_method: {type: string, enum: [lanczos, bicubic], default: lanczos}
          additionalProperties: true
          examples: [{seed: 42, color_correction: wavelet, resize_method: lanczos}]
    Job:
      type: object
      properties:
        id: {type: string}
        status: {type: string, enum: [QUEUED, ASSIGNED, RUNNING, UPLOADING_RESULT, SUCCEEDED, FAILED, CANCELED]}
        workflow_id: {type: string}
        input_width: {type: integer}
        input_height: {type: integer}
        output_width: {type: integer}
        output_height: {type: integer}
        assigned_node_id: {type: string}
        processing_seconds: {type: number}
        billed_mp: {type: number}
        price_per_mp_rmb: {type: number}
        billing_mode: {type: string, enum: [output_mp, incremental_mp]}
        charge_rmb: {type: number}
        result_sha256: {type: string}
        waiting_reason: {type: string}
        error_code: {type: string}
        error_message: {type: string}
        created_at: {type: string, format: date-time}
        finished_at: {type: string, format: date-time}
    Pagination:
      type: object
      properties:
        total: {type: integer}
        limit: {type: integer}
        offset: {type: integer}
    Capabilities:
      type: object
      properties:
        api_version: {type: string}
        workflow_id: {type: string}
        workflow_version: {type: string}
        model: {type: string}
        precision: {type: string}
        image_parameters: {type: object}
        global_safe_output_mp: {type: number}
        price_per_mp_rmb: {type: number}
        billing_mode: {type: string, enum: [output_mp, incremental_mp]}
        max_input_mp: {type: number}
        max_input_side: {type: integer}
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code: {type: string}
            message: {type: string}
  responses:
    BadRequest:
      description: Invalid request or job state
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    Unauthorized:
      description: Invalid API key
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    RateLimited:
      description: Per-key request rate exceeded
      headers:
        Retry-After: {schema: {type: integer}}
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    Unavailable:
      description: Queue, synchronous concurrency, database, or storage is temporarily unavailable
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
