{
  "openapi": "3.1.0",
  "info": {
    "title": "RENDERCAD Public API",
    "description": "Public REST API for RENDERCAD photorealistic product rendering. Endpoints are action-routed: the `action` query parameter is part of each path below. Authenticate with an API token from Settings via `Authorization: Bearer rendercad_...`. All error responses include a human-readable `error` string and a stable snake_case `error_code`. Full narrative documentation: https://rendercad.ai/developers",
    "version": "1.0.0",
    "contact": {
      "name": "RENDERCAD Support",
      "email": "help@rendercad.ai",
      "url": "https://rendercad.ai/developers"
    }
  },
  "servers": [
    { "url": "https://rendercad.ai", "description": "Production" }
  ],
  "security": [{ "bearerAuth": [] }],
  "tags": [
    { "name": "Image Rendering", "description": "Staged image render flow: create_job -> presign_upload -> upload to R2 -> finalize_job -> poll status" },
    { "name": "Render Management", "description": "History, delete, labels, favorites, folders" },
    { "name": "Video Generation", "description": "Video jobs from completed image renders or uploaded stills, plus format conversion" },
    { "name": "Webhooks", "description": "Customer webhook registration, test delivery, and delivery log" },
    { "name": "Discovery", "description": "Public catalogs of models, conditions, backgrounds, textures, and video options (no auth required)" }
  ],
  "paths": {
    "/backend/render.php?action=create_job": {
      "post": {
        "tags": ["Image Rendering"],
        "summary": "Create an image render job",
        "description": "Creates a pending render job and returns presign targets. Requires render-only or full-access scope. Pass `request_id` for idempotent retries: repeating the same request_id returns the existing job instead of creating and charging a second one.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "quality": { "type": "string", "enum": ["standard", "4k"], "default": "standard" },
                  "model": { "type": "string", "enum": ["classic", "pro"], "default": "classic", "description": "classic = Fast engine (1 credit), pro = Realistic engine (2 credits). 4k forces pro." },
                  "mode": { "type": "string", "enum": ["preserve", "creative", "freeform"], "default": "preserve" },
                  "background_style": { "type": "string", "description": "Background id from discovery, custom:RRGGBB, or auto" },
                  "condition": { "type": "string", "description": "Condition id from discovery or auto" },
                  "prompt": { "type": "string", "description": "Optional custom prompt" },
                  "request_id": { "type": "string", "maxLength": 80, "pattern": "^[A-Za-z0-9_-]+$", "description": "Client-generated idempotency key" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Job created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateJobResponse" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/LimitExceeded" }
        }
      }
    },
    "/backend/render.php?action=presign_upload": {
      "post": {
        "tags": ["Image Rendering"],
        "summary": "Presign an input upload",
        "description": "Returns a presigned PUT URL for uploading the source image directly to storage.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["job_id", "slot", "content_type"],
                "properties": {
                  "job_id": { "type": "string" },
                  "slot": { "type": "string", "description": "Upload slot, e.g. input_main" },
                  "content_type": { "type": "string", "description": "Allowlisted image content type (image/jpeg, image/png, image/webp)" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Presigned URL", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "upload_url": { "type": "string" }, "expires_in": { "type": "integer" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/backend/render.php?action=finalize_job": {
      "post": {
        "tags": ["Image Rendering"],
        "summary": "Finalize and queue a render job",
        "description": "Call after uploading the input image. Validates the upload, charges credits, and queues the render.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "type": "object", "required": ["job_id"], "properties": { "job_id": { "type": "string" } } }
            }
          }
        },
        "responses": {
          "200": { "description": "Queued", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "job_id": { "type": "string" }, "status": { "type": "string" }, "queue_depth": { "type": "integer" }, "estimated_wait_seconds": { "type": "integer" } } } } } },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/LimitExceeded" }
        }
      }
    },
    "/backend/render.php?action=status": {
      "get": {
        "tags": ["Image Rendering"],
        "summary": "Get render job status",
        "description": "Single id returns a flat job object. Up to 100 comma-separated ids return `{success, jobs: [...]}` with per-id results; unknown ids yield entries with `error_code: job_not_found` without failing the request. Requires read-only scope or higher.",
        "parameters": [
          { "name": "job_id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Job id, or comma-separated list of up to 100 ids" }
        ],
        "responses": {
          "200": { "description": "Job status (flat object for a single id, jobs array for a batch)", "content": { "application/json": { "schema": { "oneOf": [ { "$ref": "#/components/schemas/JobStatus" }, { "type": "object", "properties": { "success": { "type": "boolean" }, "jobs": { "type": "array", "items": { "$ref": "#/components/schemas/JobStatus" } } } } ] } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/backend/render.php?action=history": {
      "get": {
        "tags": ["Render Management"],
        "summary": "Paginated render history",
        "parameters": [
          { "name": "page", "in": "query", "schema": { "type": "integer", "default": 1 } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 10, "maximum": 100, "default": 50 } },
          { "name": "filter", "in": "query", "schema": { "type": "string", "enum": ["all", "image", "video"], "default": "all" } },
          { "name": "search", "in": "query", "schema": { "type": "string", "maxLength": 200 }, "description": "Substring match against the render prompt" },
          { "name": "favorites", "in": "query", "schema": { "type": "string", "enum": ["1"] }, "description": "Favorited renders only" },
          { "name": "folder", "in": "query", "schema": { "type": "string", "maxLength": 80 }, "description": "Exact folder name" },
          { "name": "date_from", "in": "query", "schema": { "type": "string", "format": "date" } },
          { "name": "date_to", "in": "query", "schema": { "type": "string", "format": "date" } },
          { "name": "sort", "in": "query", "schema": { "type": "string", "enum": ["newest", "oldest"], "default": "newest" } }
        ],
        "responses": {
          "200": { "description": "History page", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HistoryResponse" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/backend/render.php?action=delete": {
      "post": {
        "tags": ["Render Management"],
        "summary": "Delete a render",
        "description": "Removes the job's files from storage; the database record is kept for account history. Active jobs return 409. Requires delete or full-access scope.",
        "requestBody": { "$ref": "#/components/requestBodies/JobIdBody" },
        "responses": {
          "200": { "$ref": "#/components/responses/SimpleSuccess" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/backend/render.php?action=set_label": {
      "post": {
        "tags": ["Render Management"],
        "summary": "Set or clear a render label",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["job_id"], "properties": { "job_id": { "type": "string" }, "label": { "type": "string", "maxLength": 120, "description": "Empty string clears the label" } } } } }
        },
        "responses": {
          "200": { "description": "Updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "job_id": { "type": "string" }, "label": { "type": ["string", "null"] } } } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/backend/render.php?action=set_favorite": {
      "post": {
        "tags": ["Render Management"],
        "summary": "Favorite or unfavorite a render",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["job_id", "favorite"], "properties": { "job_id": { "type": "string" }, "favorite": { "type": "boolean" } } } } }
        },
        "responses": {
          "200": { "description": "Updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "job_id": { "type": "string" }, "is_favorite": { "type": "boolean" } } } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/backend/render.php?action=set_folder": {
      "post": {
        "tags": ["Render Management"],
        "summary": "Move a render into a folder",
        "description": "Assigning a new folder name creates the folder implicitly. Empty folder removes the render from its folder.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["job_id"], "properties": { "job_id": { "type": "string" }, "folder": { "type": "string", "maxLength": 80 } } } } }
        },
        "responses": {
          "200": { "description": "Updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "job_id": { "type": "string" }, "folder": { "type": ["string", "null"] } } } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/backend/video.php?action=create_job": {
      "post": {
        "tags": ["Video Generation"],
        "summary": "Create a video generation job",
        "description": "Creates a video job from a completed image render or an uploaded still. Supports the same `request_id` idempotency as image create_job. Rate limit: 10 requests / 5 minutes.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "source_job_id": { "type": "string", "description": "Completed image render to animate" },
                  "duration": { "type": "integer", "enum": [4, 6, 8], "description": "Seconds; 8/12/16 credits respectively" },
                  "prompt": { "type": "string" },
                  "request_id": { "type": "string", "maxLength": 80, "pattern": "^[A-Za-z0-9_-]+$" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Job created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateJobResponse" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/LimitExceeded" }
        }
      }
    },
    "/backend/video.php?action=finalize_job": {
      "post": {
        "tags": ["Video Generation"],
        "summary": "Finalize and queue a video job",
        "requestBody": { "$ref": "#/components/requestBodies/JobIdBody" },
        "responses": {
          "200": { "$ref": "#/components/responses/SimpleSuccess" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      }
    },
    "/backend/video.php?action=convert": {
      "post": {
        "tags": ["Video Generation"],
        "summary": "Convert a completed video to another format",
        "description": "Rate limit: 20 requests / 5 minutes.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["video_url", "format"], "properties": { "video_url": { "type": "string" }, "format": { "type": "string", "enum": ["mp4", "webm", "gif"] } } } } }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/SimpleSuccess" },
          "400": { "$ref": "#/components/responses/BadRequest" }
        }
      }
    },
    "/backend/video.php?action=status": {
      "get": {
        "tags": ["Video Generation"],
        "summary": "Get video job status",
        "parameters": [
          { "name": "job_id", "in": "query", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Video status", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string" }, "status_detail": { "type": "string" }, "video_url": { "type": ["string", "null"] } } } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/backend/webhooks.php?action=register": {
      "post": {
        "tags": ["Webhooks"],
        "summary": "Register a webhook endpoint",
        "description": "HTTPS URLs only; maximum 5 webhooks per account. Deliveries are signed with an HMAC secret returned once at registration. Delivery attempts time out after 8 seconds.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["url", "events"], "properties": { "url": { "type": "string", "format": "uri" }, "events": { "type": "array", "items": { "type": "string", "enum": ["render.completed", "render.failed", "video.completed", "video.failed", "credits.low", "credits.exhausted"] } } } } } }
        },
        "responses": {
          "200": { "description": "Registered; secret is shown only once", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "id": { "type": "integer" }, "secret": { "type": "string" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }
        }
      }
    },
    "/backend/webhooks.php?action=list": {
      "get": {
        "tags": ["Webhooks"],
        "summary": "List registered webhooks",
        "responses": {
          "200": { "description": "Webhook list", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "webhooks": { "type": "array", "items": { "type": "object" } } } } } } }
        }
      }
    },
    "/backend/webhooks.php?action=delete": {
      "post": {
        "tags": ["Webhooks"],
        "summary": "Delete a webhook",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["id"], "properties": { "id": { "type": "integer" } } } } }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/SimpleSuccess" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/backend/webhooks.php?action=test": {
      "post": {
        "tags": ["Webhooks"],
        "summary": "Send a signed test event",
        "description": "Synchronously delivers a `webhook.test` event to the webhook and reports the result.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["id"], "properties": { "id": { "type": "integer" } } } } }
        },
        "responses": {
          "200": { "description": "Delivery attempt result", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "delivered": { "type": "boolean" }, "http_code": { "type": "integer" } } } } } }
        }
      }
    },
    "/backend/webhooks.php?action=deliveries": {
      "get": {
        "tags": ["Webhooks"],
        "summary": "Recent delivery log for a webhook",
        "description": "Rolling log of the last 20 delivery attempts (7 day retention).",
        "parameters": [
          { "name": "id", "in": "query", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": {
          "200": { "description": "Delivery log", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "deliveries": { "type": "array", "items": { "type": "object" } } } } } } }
        }
      }
    },
    "/backend/api_discovery.php?action=models": {
      "get": { "tags": ["Discovery"], "summary": "List render engines and costs", "security": [], "responses": { "200": { "description": "Model catalog", "content": { "application/json": { "schema": { "type": "object" } } } } } }
    },
    "/backend/api_discovery.php?action=conditions": {
      "get": { "tags": ["Discovery"], "summary": "List condition options", "security": [], "responses": { "200": { "description": "Condition catalog", "content": { "application/json": { "schema": { "type": "object" } } } } } }
    },
    "/backend/api_discovery.php?action=backgrounds": {
      "get": { "tags": ["Discovery"], "summary": "List background options", "security": [], "responses": { "200": { "description": "Background catalog", "content": { "application/json": { "schema": { "type": "object" } } } } } }
    },
    "/backend/api_discovery.php?action=textures": {
      "get": { "tags": ["Discovery"], "summary": "List texture options", "security": [], "responses": { "200": { "description": "Texture catalog", "content": { "application/json": { "schema": { "type": "object" } } } } } }
    },
    "/backend/api_discovery.php?action=video_options": {
      "get": { "tags": ["Discovery"], "summary": "List video generation options", "security": [], "responses": { "200": { "description": "Video options catalog", "content": { "application/json": { "schema": { "type": "object" } } } } } }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API token from Settings > API. Scopes: read-only, render-only, delete, full-access."
      }
    },
    "requestBodies": {
      "JobIdBody": {
        "required": true,
        "content": { "application/json": { "schema": { "type": "object", "required": ["job_id"], "properties": { "job_id": { "type": "string" } } } } }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string", "description": "Human-readable message; may be reworded" },
          "error_code": { "type": "string", "description": "Stable snake_case code for programmatic handling" }
        },
        "required": ["error"]
      },
      "CreateJobResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean" },
          "job_id": { "type": "string" },
          "status": { "type": "string" },
          "existing": { "type": "boolean", "description": "true when request_id matched an existing job (idempotent replay)" }
        }
      },
      "JobStatus": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean" },
          "job_id": { "type": "string" },
          "status": { "type": "string", "enum": ["uploading", "pending", "processing", "completed", "failed", "canceled"] },
          "status_detail": { "type": "string" },
          "output_url": { "type": ["string", "null"], "description": "Authenticated proxy URL" },
          "asset_url": { "type": ["string", "null"], "description": "Direct asset URL" },
          "input_url": { "type": ["string", "null"] },
          "error_message": { "type": ["string", "null"] },
          "error_code": { "type": "string", "description": "Present on per-id batch errors, e.g. job_not_found" }
        }
      },
      "HistoryRender": {
        "type": "object",
        "properties": {
          "job_id": { "type": "string" },
          "status": { "type": "string" },
          "render_mode": { "type": "string" },
          "model": { "type": ["string", "null"] },
          "quality_tier": { "type": ["string", "null"] },
          "input_url": { "type": ["string", "null"] },
          "output_url": { "type": ["string", "null"] },
          "prompt": { "type": ["string", "null"] },
          "label": { "type": ["string", "null"] },
          "is_favorite": { "type": "boolean" },
          "folder": { "type": ["string", "null"] },
          "created_at": { "type": ["string", "null"], "format": "date-time" },
          "completed_at": { "type": ["string", "null"], "format": "date-time" },
          "days_to_deletion": { "type": "integer" },
          "video_settings": { "type": ["object", "null"], "description": "Canonical settings object for video renders" }
        }
      },
      "HistoryResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean" },
          "renders": { "type": "array", "items": { "$ref": "#/components/schemas/HistoryRender" } },
          "folders": { "type": "array", "items": { "type": "string" } },
          "pagination": {
            "type": "object",
            "properties": {
              "page": { "type": "integer" },
              "limit": { "type": "integer" },
              "total_count": { "type": "integer" },
              "total_pages": { "type": "integer" },
              "has_next": { "type": "boolean" },
              "has_prev": { "type": "boolean" }
            }
          }
        }
      }
    },
    "responses": {
      "SimpleSuccess": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } } } } } },
      "BadRequest": { "description": "Bad request or validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Unauthorized": { "description": "Missing or invalid authentication", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Forbidden": { "description": "Insufficient scope or inactive account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "NotFound": { "description": "Unknown action or missing resource", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Conflict": { "description": "State conflict (wrong job state)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "ValidationError": { "description": "Semantic validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "LimitExceeded": { "description": "Rate limit, credit exhaustion, or capacity limit", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    }
  }
}
