openapi: 3.1.0
info:
  title: EZPhoto API
  version: "1.0"
  description: |
    Image operations API: background removal, upscaling, and AI product shots.
    Send an image (by URL or direct upload), get a signed result URL back.
    Results expire after 24 hours; originals are never stored.
  contact:
    url: https://api.ezphoto.ai/docs
servers:
  - url: https://api.ezphoto.ai
security:
  - bearerKey: []

paths:
  /v1/remove-background:
    post:
      operationId: removeBackground
      summary: Remove the background from an image (synchronous, $0.01)
      description: >
        Synchronous: the response contains the result. Typical latency is
        around one second.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        $ref: "#/components/requestBodies/ImageOnly"
      responses:
        "200":
          description: Result ready.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SyncResult"
        default:
          $ref: "#/components/responses/Problem"

  /v1/upscale:
    post:
      operationId: upscale
      summary: Upscale an image 2x or 4x (async job, $0.05)
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [image_url]
              properties:
                image_url:
                  $ref: "#/components/schemas/ImageURL"
                factor:
                  type: number
                  enum: [2, 4]
                  default: 2
                webhook_url:
                  $ref: "#/components/schemas/WebhookURL"
          multipart/form-data:
            schema:
              type: object
              required: [image]
              properties:
                image:
                  type: string
                  format: binary
                factor:
                  type: number
                  enum: [2, 4]
                webhook_url:
                  type: string
      responses:
        "202":
          description: Job accepted; poll or use the webhook.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Job"
        default:
          $ref: "#/components/responses/Problem"

  /v1/product-shot:
    post:
      operationId: productShot
      summary: Place a product photo into a styled scene (async job, $0.10)
      description: >
        Provide either a scene preset or a custom prompt. Generated images pass
        a safety check; flagged content fails the job instead of returning.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [image_url]
              properties:
                image_url:
                  $ref: "#/components/schemas/ImageURL"
                scene:
                  type: string
                  enum: [studio-white, marble-table, outdoor-lifestyle]
                prompt:
                  type: string
                  description: Custom scene description; overrides scene.
                webhook_url:
                  $ref: "#/components/schemas/WebhookURL"
          multipart/form-data:
            schema:
              type: object
              required: [image]
              properties:
                image:
                  type: string
                  format: binary
                scene:
                  type: string
                  enum: [studio-white, marble-table, outdoor-lifestyle]
                prompt:
                  type: string
                webhook_url:
                  type: string
      responses:
        "202":
          description: Job accepted; poll or use the webhook.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Job"
        default:
          $ref: "#/components/responses/Problem"

  /v1/jobs/{id}:
    get:
      operationId: getJob
      summary: Fetch the state of an async job
      description: Jobs are visible only to the API key that created them.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Current job state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Job"
        default:
          $ref: "#/components/responses/Problem"

components:
  securitySchemes:
    bearerKey:
      type: http
      scheme: bearer
      description: API key starting with ez_live_. Created in the dashboard.

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
      description: >
        Any client-chosen string. Retrying with the same key and the same
        normalized request replays the original 2xx response (with
        Idempotency-Replayed: true) without charging again. Same key with a
        different body: 422. Retry while the original is in flight: 409.
        Failed requests release the key.

  requestBodies:
    ImageOnly:
      required: true
      content:
        application/json:
          schema:
            type: object
            required: [image_url]
            properties:
              image_url:
                $ref: "#/components/schemas/ImageURL"
        multipart/form-data:
          schema:
            type: object
            required: [image]
            properties:
              image:
                type: string
                format: binary
                description: PNG, JPEG, WebP or GIF, up to 20 MB.

  responses:
    Problem:
      description: Error (RFC 7807).
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"

  schemas:
    ImageURL:
      type: string
      format: uri
      description: >
        Publicly reachable http(s) URL of the source image. Private/localhost
        URLs fail with 422 because inference providers fetch it directly.

    WebhookURL:
      type: string
      format: uri
      description: >
        http(s) URL that receives the finished job object via POST (3
        attempts: immediate, +5s, +25s; 10s timeout; any 2xx counts as
        delivered).

    SyncResult:
      type: object
      required: [request_id, op, result_url, expires_at]
      properties:
        request_id:
          type: string
          format: uuid
        op:
          type: string
          enum: [remove-background]
        result_url:
          type: string
          format: uri
          description: Signed URL of the result; valid until expires_at (24h).
        expires_at:
          type: string
          format: date-time

    Job:
      type: object
      required: [id, op, status, created_at, updated_at]
      properties:
        id:
          type: string
          format: uuid
        op:
          type: string
          enum: [upscale, product-shot]
        status:
          type: string
          enum: [queued, processing, succeeded, failed]
        result:
          type: [object, "null"]
          properties:
            result_url:
              type: string
              format: uri
            expires_at:
              type: string
              format: date-time
        error:
          type: [string, "null"]
          description: Stable human-readable message when status is failed.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Problem:
      type: object
      required: [type, title, status]
      properties:
        type:
          type: string
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
