{
  "openapi": "3.1.0",
  "info": {
    "title": "PixelFireman API",
    "version": "1.0.0",
    "description": "Hosted image and video generation API. Claude routes each image request to the cheapest engine (vector SVG or rendered photo). Authenticate with a pf_live_ key via the Authorization bearer header or the x-api-key header. Image credits never expire.",
    "contact": {
      "name": "PixelFireman",
      "url": "https://pixelfireman.com"
    }
  },
  "servers": [
    {
      "url": "https://pixelfireman.com",
      "description": "Production"
    }
  ],
  "security": [
    { "bearerAuth": [] },
    { "apiKeyHeader": [] }
  ],
  "paths": {
    "/v1/images": {
      "post": {
        "operationId": "generateImage",
        "summary": "Generate an image",
        "description": "Generate an image from a text prompt. mode 'auto' lets the server pick the cheapest engine; 'draw' returns a vector SVG (flat 1 credit); 'generate' returns a rendered photo (credits scale with megapixels).",
        "security": [
          { "bearerAuth": [] },
          { "apiKeyHeader": [] }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ImageRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Image generated.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ImageResponse" }
              }
            }
          },
          "400": {
            "description": "Missing prompt or image size exceeds the 2048x2048 cap.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SizeError" }
              }
            }
          },
          "402": {
            "description": "Not enough image credits.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/NoCreditsError" }
              }
            }
          },
          "429": {
            "description": "Rate limit or concurrency cap hit for this key.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "451": {
            "description": "Prompt blocked by moderation.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ModerationError" }
              }
            }
          },
          "500": {
            "description": "Server error.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/v1/videos": {
      "post": {
        "operationId": "generateVideo",
        "summary": "Generate a video",
        "description": "Generate a short video from a text prompt. The tier controls quality/length and how many wallet minutes it costs.",
        "security": [
          { "bearerAuth": [] },
          { "apiKeyHeader": [] }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/VideoRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Video rendered inline (common for economy/standard tiers). Body is the final clip.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/VideoResponse" }
              }
            }
          },
          "202": {
            "description": "Video is still rendering (common for premium/Kling). No minutes are billed yet. Poll the `poll` URL (GET /v1/videos/{id}) until status is 'completed'. Branch on the HTTP status: 200 = inline final clip, 202 = async job to poll.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/VideoProcessing" }
              }
            }
          },
          "400": {
            "description": "Missing prompt or unknown tier.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "402": {
            "description": "Not enough video minutes.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/NoMinutesError" }
              }
            }
          },
          "429": {
            "description": "Rate limit or concurrency cap hit for this key.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "451": {
            "description": "Prompt blocked by moderation.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ModerationError" }
              }
            }
          },
          "500": {
            "description": "Server error.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/v1/videos/{id}": {
      "get": {
        "operationId": "pollVideoJob",
        "summary": "Poll a video render job",
        "description": "Poll an async video job returned by a 202 from POST /v1/videos. Returns status 'processing' (keep polling), 'completed' (with the final `video` URL; minutes are billed exactly once, here, on completion), or a 502 when the render failed (no minutes billed). Send the same Authorization / x-api-key header.",
        "security": [
          { "bearerAuth": [] },
          { "apiKeyHeader": [] }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "The jobId returned in the 202 response (also the tail of the `poll` URL).",
            "example": "vj_8f3k2x9a"
          }
        ],
        "responses": {
          "200": {
            "description": "Job status. Either still processing, or completed with the final clip.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/VideoProcessing" },
                    { "$ref": "#/components/schemas/VideoResponse" }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Invalid or missing API key.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "403": {
            "description": "The job belongs to a different key.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "404": {
            "description": "Job not found or expired.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "502": {
            "description": "The render failed. No video minutes were billed.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/VideoFailed" }
              }
            }
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "operationId": "getUsage",
        "summary": "Get usage and balance",
        "description": "Return usage totals for this key plus the owner wallet balance (image credits and video minutes).",
        "security": [
          { "bearerAuth": [] },
          { "apiKeyHeader": [] }
        ],
        "responses": {
          "200": {
            "description": "Usage and balance.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/UsageResponse" }
              }
            }
          },
          "401": {
            "description": "Invalid or missing API key.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Send your key as: Authorization: Bearer pf_live_..."
      },
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "Alternatively send your key as: x-api-key: pf_live_..."
      }
    },
    "schemas": {
      "ImageRequest": {
        "type": "object",
        "required": ["prompt"],
        "properties": {
          "prompt": {
            "type": "string",
            "description": "Text description of the image to create."
          },
          "mode": {
            "type": "string",
            "enum": ["auto", "draw", "generate"],
            "default": "auto",
            "description": "auto = server picks cheapest engine; draw = vector SVG; generate = rendered photo."
          },
          "size": {
            "type": "string",
            "default": "1024x1024",
            "description": "WIDTHxHEIGHT. Max 2048x2048. Ignored for draw mode.",
            "example": "1024x1024"
          }
        }
      },
      "ImageResponse": {
        "type": "object",
        "description": "Lead on creditsUsed (what you were charged) and usage.creditsLeft (your remaining balance). The providerCost field is internal provider cost surfaced for transparency only and is not the customer price.",
        "required": ["creditsUsed", "format", "image"],
        "properties": {
          "creditsUsed": {
            "type": "integer",
            "description": "WHAT YOU PAY: image credits charged for this call. 1024x1024 = 1 credit; scales with megapixels (rounded up, min 1)."
          },
          "format": {
            "type": "string",
            "enum": ["url", "svg"],
            "description": "'url' when image is a hosted URL, 'svg' for draw mode."
          },
          "image": {
            "type": "string",
            "description": "Absolute image URL (format=url) or raw SVG markup (format=svg). URLs are temporary; download the bytes if you need a permanent copy."
          },
          "engine": {
            "type": "string",
            "description": "Engine that served the request (e.g. draw, or the photo engine name). Informational only."
          },
          "model": {
            "type": "string",
            "description": "Present only when a custom style model was used; echoes the model id."
          },
          "providerCost": {
            "type": "number",
            "description": "INTERNAL: raw provider cost for this call, in USD. Not your price (you pay in credits via creditsUsed). Safe to ignore."
          },
          "usage": {
            "type": "object",
            "properties": {
              "creditsLeft": {
                "type": ["integer", "null"],
                "description": "YOUR BALANCE: image credits remaining in the owner wallet after this call, or null if the key has no owner."
              },
              "images": { "type": "integer", "description": "Lifetime image count on this key (a counter, not a charge)." }
            }
          }
        }
      },
      "VideoRequest": {
        "type": "object",
        "required": ["prompt"],
        "properties": {
          "prompt": {
            "type": "string",
            "description": "Text description of the video to create."
          },
          "tier": {
            "type": "string",
            "enum": ["economy", "standard", "premium"],
            "default": "economy",
            "description": "Quality/length tier. Higher tiers cost more wallet minutes."
          }
        }
      },
      "VideoResponse": {
        "type": "object",
        "description": "A completed clip, returned inline as the 200 body from POST /v1/videos, or as the completed result when polling GET /v1/videos/{id}.",
        "required": ["format", "video", "minutesUsed"],
        "properties": {
          "status": {
            "type": "string",
            "enum": ["completed"],
            "description": "Present ('completed') when returned from the poll endpoint; omitted on the inline 200."
          },
          "tier": {
            "type": "string",
            "enum": ["economy", "standard", "premium"]
          },
          "aspect": {
            "type": "string",
            "description": "Aspect ratio of the clip, e.g. '16:9'.",
            "example": "16:9"
          },
          "format": {
            "type": "string",
            "enum": ["url"]
          },
          "video": {
            "type": "string",
            "description": "Absolute video URL. Temporary; download the bytes if you need a permanent copy."
          },
          "minutesUsed": {
            "type": "integer",
            "description": "Video minutes charged for this clip. Billed once, on completion only."
          },
          "usage": {
            "type": "object",
            "properties": {
              "minutesLeft": {
                "type": ["integer", "null"],
                "description": "Video minutes remaining in the owner wallet, or null if the key has no owner."
              }
            }
          }
        }
      },
      "VideoProcessing": {
        "type": "object",
        "description": "An async video job that is still rendering. Returned as the 202 body from POST /v1/videos and while polling GET /v1/videos/{id}. No minutes are billed until the job completes.",
        "required": ["status"],
        "properties": {
          "status": {
            "type": "string",
            "enum": ["processing"],
            "description": "Always 'processing' for this shape. Keep polling the `poll` URL."
          },
          "tier": {
            "type": "string",
            "enum": ["economy", "standard", "premium"]
          },
          "aspect": {
            "type": "string",
            "description": "Aspect ratio of the clip being rendered.",
            "example": "16:9"
          },
          "jobId": {
            "type": "string",
            "description": "Job identifier. Also the tail of the `poll` URL.",
            "example": "vj_8f3k2x9a"
          },
          "poll": {
            "type": "string",
            "description": "Absolute URL to poll with GET until status is 'completed'.",
            "example": "https://pixelfireman.com/v1/videos/vj_8f3k2x9a"
          },
          "message": {
            "type": "string",
            "description": "Human-readable hint, e.g. 'Video is still rendering. Poll the job URL until status is completed.'"
          }
        }
      },
      "VideoFailed": {
        "type": "object",
        "description": "A render that failed. No video minutes were billed.",
        "required": ["status", "error"],
        "properties": {
          "status": {
            "type": "string",
            "enum": ["failed"]
          },
          "error": {
            "type": "string",
            "description": "Human-readable failure reason."
          }
        }
      },
      "UsageResponse": {
        "type": "object",
        "properties": {
          "name": { "type": "string", "description": "Key name." },
          "images": { "type": "integer", "description": "Lifetime images on this key." },
          "providerCost": { "type": "number", "description": "INTERNAL: lifetime raw provider cost on this key, USD. Not your price." },
          "lastUsed": {
            "type": ["string", "null"],
            "description": "Timestamp of last use, or null."
          },
          "daily": {
            "type": "object",
            "description": "Per-day usage counters for this key.",
            "additionalProperties": true
          },
          "minutes": {
            "type": ["integer", "null"],
            "description": "Video minutes remaining in the owner wallet, or null if the key has no owner."
          },
          "credits": {
            "type": ["integer", "null"],
            "description": "Image credits remaining in the owner wallet, or null if the key has no owner."
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string", "description": "Human-readable error message." }
        },
        "required": ["error"]
      },
      "NoCreditsError": {
        "type": "object",
        "properties": {
          "error": { "type": "string" },
          "noCredits": { "type": "boolean", "enum": [true] },
          "creditsNeeded": {
            "type": "integer",
            "description": "Credits required for the requested size (present when the size needs more than you have)."
          }
        },
        "required": ["error", "noCredits"]
      },
      "NoMinutesError": {
        "type": "object",
        "properties": {
          "error": { "type": "string" },
          "noMinutes": { "type": "boolean", "enum": [true] }
        },
        "required": ["error", "noMinutes"]
      },
      "ModerationError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Begins with 'Blocked: ' followed by the moderation reason."
          },
          "moderated": { "type": "boolean", "enum": [true] }
        },
        "required": ["error", "moderated"]
      },
      "SizeError": {
        "type": "object",
        "properties": {
          "error": { "type": "string" },
          "maxSize": {
            "type": "string",
            "example": "2048x2048",
            "description": "Present when the requested size exceeds the cap."
          }
        },
        "required": ["error"]
      }
    }
  }
}
