{
  "openapi": "3.1.0",
  "info": {
    "title": "Pedra API",
    "version": "1.0.0",
    "summary": "AI photo and video editing for real estate.",
    "description": "Programmatic access to Pedra's AI real estate image and video transformations — virtual staging, renovation, prompt-based editing, image enhancement, sky replacement, blurring, object removal, and listing-video generation.\n\n## Authentication\nEvery request is authenticated with an API key passed as the `apiKey` field in the JSON request body (not an HTTP header). Get a key by signing up at https://app.pedra.ai and opening Settings → API.\n\n## Conventions\n- All endpoints are `POST` with a JSON body and return JSON (`Content-Type: application/json`).\n- Image inputs (`imageUrl`, `maskUrl`, etc.) accept either a publicly accessible HTTPS URL or a base64 data URI (`data:image/png;base64,...`). Pedra stops downloading a remote URL after ~20s, so prefer base64 for auth-walled or short-lived presigned URLs.\n- All calls are **synchronous** — there is no polling or job ID. Image endpoints return in ~10–30s; `create_video` blocks until the video is rendered (~2–10 min), so set a generous client timeout.\n- Errors are returned with a non-200 status and a JSON body of the shape `{ \"error\": \"...\" }`.\n\nSee the full docs at https://pedra.ai/api-documentation, the SDKs at https://pedra.ai/api-documentation/sdks, and the MCP server at https://pedra.ai/api-documentation/mcp.",
    "contact": {
      "name": "Pedra API support",
      "url": "https://pedra.ai/api-documentation",
      "email": "felix@pedra.ai"
    },
    "license": {
      "name": "Proprietary — see Terms",
      "url": "https://pedra.ai/legal"
    },
    "termsOfService": "https://pedra.ai/legal"
  },
  "security": [],
  "servers": [
    {
      "url": "https://app.pedra.ai",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Pedra API documentation",
    "url": "https://pedra.ai/api-documentation"
  },
  "tags": [
    { "name": "Image enhancement", "description": "Enhance lighting/color, correct perspective, replace skies, and blur sensitive content." },
    { "name": "Virtual staging", "description": "Furnish, empty, renovate, prompt-edit, and remove objects from room photos." },
    { "name": "Video", "description": "Generate listing videos from photos." },
    { "name": "Account", "description": "Account-level reads and feedback." }
  ],
  "paths": {
    "/api/enhance": {
      "post": {
        "tags": ["Image enhancement"],
        "summary": "Auto-enhance a real estate photo",
        "description": "Improves lighting, color, and contrast on a listing photo. Costs 1 credit. Responds in ~10s.",
        "operationId": "enhance",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey", "imageUrl"],
                "properties": {
                  "apiKey": { "$ref": "#/components/schemas/ApiKey" },
                  "imageUrl": { "$ref": "#/components/schemas/ImageInput" },
                  "highFidelity": {
                    "type": "boolean",
                    "default": false,
                    "description": "Non-generative enhancement: adjusts only light and sharpness, keeping colors, materials, object sizes, and framing exactly as in the original photo. Best when the result must faithfully match the original. When true, preserveOriginalFraming is ignored."
                  },
                  "preserveOriginalFraming": {
                    "type": "boolean",
                    "default": false,
                    "description": "Keep the original framing, aspect ratio, and resolution exactly (no edge crop). The enhancement is still AI-generated, so colors/materials may shift slightly — use highFidelity if they must stay identical. Ignored when highFidelity is true."
                  },
                  "propertyId": { "$ref": "#/components/schemas/PropertyId" }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "imageUrl": "https://example.com/listing-photo.jpg",
                "highFidelity": true
              }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/ImageArrayResult" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/InsufficientCredits" },
          "404": { "$ref": "#/components/responses/InvalidApiKey" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    },
    "/api/enhance_and_correct_perspective": {
      "post": {
        "tags": ["Image enhancement"],
        "summary": "Enhance a photo and straighten vertical lines",
        "description": "Auto-enhances the photo and corrects perspective so vertical lines are straight. Costs 1 credit. Responds in ~15s.",
        "operationId": "enhanceAndCorrectPerspective",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey", "imageUrl"],
                "properties": {
                  "apiKey": { "$ref": "#/components/schemas/ApiKey" },
                  "imageUrl": { "$ref": "#/components/schemas/ImageInput" },
                  "highFidelity": {
                    "type": "boolean",
                    "default": false,
                    "description": "Non-generative enhancement plus perspective correction: adjusts only light, sharpness, and perspective, keeping colors, materials, and object sizes exactly as in the original. When true, preserveOriginalFraming is ignored."
                  },
                  "preserveOriginalFraming": {
                    "type": "boolean",
                    "default": false,
                    "description": "Keep the original framing, aspect ratio, and resolution exactly (no edge crop). The enhancement is still AI-generated, so colors/materials may shift slightly — use highFidelity if they must stay identical. Ignored when highFidelity is true."
                  },
                  "propertyId": { "$ref": "#/components/schemas/PropertyId" }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "imageUrl": "https://example.com/listing-photo.jpg"
              }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/ImageArrayResult" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/InsufficientCredits" },
          "404": { "$ref": "#/components/responses/InvalidApiKey" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    },
    "/api/sky_blue": {
      "post": {
        "tags": ["Image enhancement"],
        "summary": "Replace the sky in an exterior photo",
        "description": "Swaps a dull or overcast sky for a chosen style. Costs 1 credit. Responds in ~10s.",
        "operationId": "skyBlue",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey", "imageUrl"],
                "properties": {
                  "apiKey": { "$ref": "#/components/schemas/ApiKey" },
                  "imageUrl": { "$ref": "#/components/schemas/ImageInput" },
                  "skyStyle": {
                    "type": "string",
                    "default": "sunny",
                    "enum": ["sunny", "sunny-no-reflections", "sunrise", "dawn", "night"],
                    "description": "Target sky look."
                  },
                  "propertyId": { "$ref": "#/components/schemas/PropertyId" }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "imageUrl": "https://example.com/house-exterior.jpg",
                "skyStyle": "sunny"
              }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/ImageObjectResult" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/InsufficientCredits" },
          "404": { "$ref": "#/components/responses/InvalidApiKey" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    },
    "/api/blur": {
      "post": {
        "tags": ["Image enhancement"],
        "summary": "Blur sensitive content in a photo",
        "description": "Blurs faces, license plates, logos, or other sensitive content. Costs 1 credit. Responds in ~10s.",
        "operationId": "blur",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey", "imageUrl", "objectsToBlur"],
                "properties": {
                  "apiKey": { "$ref": "#/components/schemas/ApiKey" },
                  "imageUrl": { "$ref": "#/components/schemas/ImageInput" },
                  "objectsToBlur": {
                    "type": "string",
                    "description": "Comma-separated list of what to blur, e.g. \"faces\", \"license plates\", \"logos\", or \"faces, license plates\".",
                    "examples": ["faces, license plates"]
                  },
                  "propertyId": { "$ref": "#/components/schemas/PropertyId" }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "imageUrl": "https://example.com/street-view.jpg",
                "objectsToBlur": "faces, license plates"
              }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/ImageObjectResult" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/InsufficientCredits" },
          "404": { "$ref": "#/components/responses/InvalidApiKey" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    },
    "/api/empty_room": {
      "post": {
        "tags": ["Virtual staging"],
        "summary": "Remove all furniture from a room",
        "description": "Empties a furnished room, returning a clean empty space. Costs 1 credit. Responds in ~30s.",
        "operationId": "emptyRoom",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey", "imageUrl"],
                "properties": {
                  "apiKey": { "$ref": "#/components/schemas/ApiKey" },
                  "imageUrl": { "$ref": "#/components/schemas/ImageInput" },
                  "propertyId": { "$ref": "#/components/schemas/PropertyId" }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "imageUrl": "https://example.com/furnished-room.jpg"
              }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/ImageArrayResult" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/InsufficientCredits" },
          "404": { "$ref": "#/components/responses/InvalidApiKey" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    },
    "/api/furnish": {
      "post": {
        "tags": ["Virtual staging"],
        "summary": "Furnish an empty room",
        "description": "Adds furniture and decor to an empty room in a chosen style. Costs 1 credit at High creativity, 2 at Medium. Responds in ~30s.",
        "operationId": "furnish",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey", "imageUrl", "roomType", "style"],
                "properties": {
                  "apiKey": { "$ref": "#/components/schemas/ApiKey" },
                  "imageUrl": { "$ref": "#/components/schemas/ImageInput" },
                  "roomType": { "$ref": "#/components/schemas/RoomType" },
                  "style": { "$ref": "#/components/schemas/Style" },
                  "creativity": { "$ref": "#/components/schemas/Creativity" },
                  "propertyId": { "$ref": "#/components/schemas/PropertyId" }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "imageUrl": "https://example.com/empty-bedroom.jpg",
                "roomType": "Bedroom",
                "style": "Minimalist",
                "creativity": "Medium"
              }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/ImageArrayResult" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/InsufficientCredits" },
          "404": { "$ref": "#/components/responses/InvalidApiKey" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    },
    "/api/renovation": {
      "post": {
        "tags": ["Virtual staging"],
        "summary": "Renovate a room in a new style",
        "description": "Re-renovates an existing room in a chosen style, optionally re-furnishing it in the same call. Costs 1 credit at High creativity, 2 at Medium. Responds in ~30s.",
        "operationId": "renovation",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey", "imageUrl", "style"],
                "properties": {
                  "apiKey": { "$ref": "#/components/schemas/ApiKey" },
                  "imageUrl": { "$ref": "#/components/schemas/ImageInput" },
                  "style": { "$ref": "#/components/schemas/Style" },
                  "preserveWindows": {
                    "type": "boolean",
                    "default": false,
                    "description": "When true, keeps windows and their views unchanged."
                  },
                  "furnish": {
                    "type": "boolean",
                    "default": false,
                    "description": "When true, also furnishes the renovated room in the same call."
                  },
                  "roomType": {
                    "type": "string",
                    "default": "Auto",
                    "description": "Used when `furnish` is true. \"Auto\" lets Pedra detect the room type. Otherwise a predefined room type (\"Living room\", \"Dining + Living room\", \"Bedroom\", \"Dining room\", \"Terrace\", \"Entrance\", \"Office\", \"Bathroom\") or any custom string."
                  },
                  "creativity": { "$ref": "#/components/schemas/Creativity" },
                  "propertyId": { "$ref": "#/components/schemas/PropertyId" }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "imageUrl": "https://example.com/dated-kitchen.jpg",
                "style": "Modern",
                "preserveWindows": true,
                "furnish": false,
                "creativity": "Medium"
              }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/ImageArrayResult" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/InsufficientCredits" },
          "404": { "$ref": "#/components/responses/InvalidApiKey" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    },
    "/api/edit_via_prompt": {
      "post": {
        "tags": ["Virtual staging"],
        "summary": "Edit a photo with a text prompt",
        "description": "Applies a plain-English editing instruction to a photo. Costs 2 credits. Responds in ~15s.",
        "operationId": "editViaPrompt",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey", "imageUrl", "prompt"],
                "properties": {
                  "apiKey": { "$ref": "#/components/schemas/ApiKey" },
                  "imageUrl": { "$ref": "#/components/schemas/ImageInput" },
                  "prompt": {
                    "type": "string",
                    "description": "Plain-English instruction for the edit.",
                    "examples": ["Add a modern chandelier to the ceiling"]
                  },
                  "propertyId": { "$ref": "#/components/schemas/PropertyId" }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "imageUrl": "https://example.com/living-room.jpg",
                "prompt": "Repaint the walls white and remove the rug"
              }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/ImageObjectResult" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/InsufficientCredits" },
          "404": { "$ref": "#/components/responses/InvalidApiKey" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    },
    "/api/remove_object": {
      "post": {
        "tags": ["Virtual staging"],
        "summary": "Remove objects using a mask",
        "description": "Removes the masked region from a photo and inpaints the background. Costs 1 credit. Responds in ~15s.",
        "operationId": "removeObject",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey", "imageUrl", "maskUrl"],
                "properties": {
                  "apiKey": { "$ref": "#/components/schemas/ApiKey" },
                  "imageUrl": { "$ref": "#/components/schemas/ImageInput" },
                  "maskUrl": {
                    "allOf": [{ "$ref": "#/components/schemas/ImageInput" }],
                    "description": "Mask image where white pixels mark the area to remove and black pixels are preserved."
                  },
                  "propertyId": { "$ref": "#/components/schemas/PropertyId" }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "imageUrl": "https://example.com/room.jpg",
                "maskUrl": "https://example.com/room-mask.png"
              }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/ImageObjectResult" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/InsufficientCredits" },
          "404": { "$ref": "#/components/responses/InvalidApiKey" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    },
    "/api/create_video": {
      "post": {
        "tags": ["Video"],
        "summary": "Generate a listing video from photos",
        "description": "Renders a listing video from an ordered list of frames, with per-frame effects, subtitles, music, voiceover, property characteristics, and branding. Costs 5 credits per animated frame (0 for static frames). Synchronous — blocks until the video is rendered (~2–10 min), so set a generous client timeout.",
        "operationId": "createVideo",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey", "images"],
                "properties": {
                  "apiKey": { "$ref": "#/components/schemas/ApiKey" },
                  "images": {
                    "type": "array",
                    "minItems": 1,
                    "description": "Ordered list of frames that make up the video.",
                    "items": { "$ref": "#/components/schemas/VideoFrame" }
                  },
                  "isVertical": {
                    "type": "boolean",
                    "default": false,
                    "description": "Render 9:16 vertical (Reels/TikTok) instead of horizontal."
                  },
                  "propertyCharacteristics": {
                    "type": "array",
                    "description": "Property details shown on frames whose `characteristics.enabled` is true. Built-in labels render with an icon: Bedrooms, Bathrooms, Surface, Price, Location, Parking, Heating, Outdoor. Custom labels render without an icon.",
                    "items": { "$ref": "#/components/schemas/PropertyCharacteristic" }
                  },
                  "music": { "$ref": "#/components/schemas/Music" },
                  "voice": { "$ref": "#/components/schemas/Voice" },
                  "branding": { "$ref": "#/components/schemas/Branding" },
                  "endingTitle": {
                    "type": "string",
                    "description": "Headline shown on the final card."
                  },
                  "endingSubtitle": {
                    "type": "string",
                    "description": "Subtitle shown on the final card, e.g. a CTA or contact details."
                  },
                  "propertyId": { "$ref": "#/components/schemas/PropertyId" }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "images": [
                  { "imageUrl": "https://example.com/exterior.jpg", "effect": "zoom-in", "subtitle": "Renovated 2-bed in Barcelona", "characteristics": { "enabled": true } },
                  { "imageUrl": "https://example.com/living-room.jpg", "effect": "zoom-out", "subtitle": "Bright open-plan living space" },
                  { "imageUrl": "https://example.com/before.jpg", "secondImageUrl": "https://example.com/after.jpg", "effect": "transition", "title": "Before & After" }
                ],
                "isVertical": false,
                "propertyCharacteristics": [
                  { "label": "Price", "value": "€450,000" },
                  { "label": "Bedrooms", "value": "2" },
                  { "label": "Surface", "value": "85 m²" }
                ],
                "music": { "enabled": true, "track": "cinematic" },
                "endingTitle": "Book a viewing",
                "endingSubtitle": "felix@pedra.ai"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Video rendered.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/VideoResult" },
                "example": {
                  "message": "Video created successfully",
                  "videoId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                  "videoUrl": "https://img.pedra.ai/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/InsufficientCredits" },
          "404": { "$ref": "#/components/responses/InvalidApiKey" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    },
    "/api/update_video": {
      "post": {
        "tags": ["Video"],
        "summary": "Edit an existing video without re-rendering unchanged clips",
        "description": "Edits a video (by videoId) created with create_video. Clips whose source photo + effect (+ second photo for transitions) are unchanged reuse their already-rendered clip — no re-animation, no credits. Only new or changed non-static frames re-animate at 5 credits each. Omit `images` to change only audio/text/branding (a free re-stitch). Omitted music/voice/branding/ending fields keep their stored values. Synchronous — blocks until the new video is rendered. If the video was created with a `propertyId`, that link is inherited automatically — no need to pass it again.",
        "operationId": "updateVideo",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey", "videoId"],
                "properties": {
                  "apiKey": { "$ref": "#/components/schemas/ApiKey" },
                  "videoId": { "type": "string", "description": "Id of the video to edit (the videoId returned by create_video)." },
                  "images": {
                    "type": "array",
                    "description": "Full ordered frame list to rebuild the timeline; matching photo+effect clips are reused. Omit to keep the current timeline and edit only audio/text/branding.",
                    "items": { "$ref": "#/components/schemas/VideoFrame" }
                  },
                  "isVertical": { "type": "boolean", "description": "Render 9:16 vertical instead of horizontal (only applied when `images` is sent)." },
                  "propertyCharacteristics": {
                    "type": "array",
                    "items": { "$ref": "#/components/schemas/PropertyCharacteristic" }
                  },
                  "music": { "$ref": "#/components/schemas/Music" },
                  "voice": { "$ref": "#/components/schemas/Voice" },
                  "branding": { "$ref": "#/components/schemas/Branding" },
                  "endingTitle": { "type": "string", "description": "Headline shown on the final card." },
                  "endingSubtitle": { "type": "string", "description": "Subtitle shown on the final card." }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "videoId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                "music": { "enabled": true, "track": "upbeat" }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Video re-rendered.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/VideoResult" },
                "example": {
                  "message": "Video updated successfully",
                  "videoId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                  "videoUrl": "https://img.pedra.ai/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/InsufficientCredits" },
          "404": { "$ref": "#/components/responses/InvalidApiKey" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    },
    "/api/generate_voice_script": {
      "post": {
        "tags": ["Video"],
        "summary": "Generate a voiceover script from photos",
        "description": "Writes a short, natural voiceover script from listing photos and optional property facts, using vision so the script reflects what's in the images. Returns the script text — pass it to /api/generate_voice. Free.",
        "operationId": "generateVoiceScript",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey"],
                "properties": {
                  "apiKey": { "$ref": "#/components/schemas/ApiKey" },
                  "images": {
                    "type": "array",
                    "description": "Photos to base the script on: URL strings or { imageUrl } objects.",
                    "items": { "type": "string" }
                  },
                  "propertyCharacteristics": {
                    "type": "array",
                    "items": { "$ref": "#/components/schemas/PropertyCharacteristic" }
                  },
                  "language": { "type": "string", "default": "English", "description": "Script language. See /api/music_library for accepted values." }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "images": ["https://example.com/exterior.jpg", "https://example.com/living-room.jpg"],
                "propertyCharacteristics": [{ "label": "Bedrooms", "value": "2" }],
                "language": "English"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Script generated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": { "type": "string", "examples": ["Script generated successfully"] },
                    "script": { "type": "string", "description": "The generated voiceover script." }
                  }
                },
                "example": {
                  "message": "Script generated successfully",
                  "script": "Welcome to this bright, renovated two-bedroom apartment."
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/InvalidApiKey" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    },
    "/api/generate_voice": {
      "post": {
        "tags": ["Video"],
        "summary": "Render a voiceover from a script",
        "description": "Renders a script to a voiceover audio track via text-to-speech, returning an audioId to pass to a video's voice.audioId (which also drives synced subtitles). Costs 1 credit. Text limit is 1000 characters.",
        "operationId": "generateVoice",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey", "text"],
                "properties": {
                  "apiKey": { "$ref": "#/components/schemas/ApiKey" },
                  "text": { "type": "string", "maxLength": 1000, "description": "The script to narrate." },
                  "language": { "type": "string", "default": "English", "description": "Voice language (selects the speaker). See /api/music_library for accepted values." }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "text": "Welcome to this bright, renovated two-bedroom apartment.",
                "language": "English"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Voiceover rendered.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": { "type": "string", "examples": ["Voice generated successfully"] },
                    "audioId": { "type": "string", "description": "Pass to a video's voice.audioId to attach the narration." },
                    "audioUrl": { "type": "string", "description": "Public URL of the rendered mp3." },
                    "alignmentUrl": { "type": "string", "description": "URL of the word-alignment JSON used for synced subtitles." },
                    "duration": { "type": "integer", "description": "Approximate duration in seconds." }
                  }
                },
                "example": {
                  "message": "Voice generated successfully",
                  "audioId": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
                  "audioUrl": "https://img.pedra.ai/audio/b2c3d4e5-f6a7-8901-bcde-f23456789012.mp3",
                  "alignmentUrl": "https://img.pedra.ai/audio/b2c3d4e5-f6a7-8901-bcde-f23456789012.alignment.json",
                  "duration": 7
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/InsufficientCredits" },
          "404": { "$ref": "#/components/responses/InvalidApiKey" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    },
    "/api/music_library": {
      "get": {
        "tags": ["Video"],
        "summary": "List the background-music catalog",
        "description": "Returns the valid `music.track` values (genre keys) with display labels, plus the languages accepted by the voiceover endpoints. Read-only — never deducts credits. Also available as POST.",
        "operationId": "musicLibrary",
        "responses": {
          "200": {
            "description": "Music catalog and voice languages.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tracks": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "track": { "type": "string", "description": "Genre key for music.track." },
                          "label": { "type": "string", "description": "Display label." }
                        }
                      }
                    },
                    "variantsPerTrack": { "type": "integer", "description": "Number of shuffled variants per genre." },
                    "defaultTrack": { "type": "string", "description": "Default genre key." },
                    "voiceLanguages": { "type": "array", "items": { "type": "string" }, "description": "Languages accepted by generate_voice / generate_voice_script." }
                  }
                },
                "example": {
                  "tracks": [
                    { "track": "acoustic", "label": "Acoustic & Warm" },
                    { "track": "chill", "label": "Chill Beats" },
                    { "track": "cinematic", "label": "Cinematic & Orchestral" },
                    { "track": "electronic", "label": "Modern Electronic" },
                    { "track": "upbeat", "label": "Upbeat & Energetic" }
                  ],
                  "variantsPerTrack": 6,
                  "defaultTrack": "chill",
                  "voiceLanguages": ["English", "Español", "Français", "Deutsch", "Português", "Italiano", "Nederlands", "Polski"]
                }
              }
            }
          },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    },
    "/api/list_properties": {
      "post": {
        "tags": ["Properties"],
        "summary": "List the account's properties",
        "description": "Returns the account's properties (newest first), each with a photo count and an appUrl that opens it in the Pedra web app.",
        "operationId": "listProperties",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey"],
                "properties": { "apiKey": { "$ref": "#/components/schemas/ApiKey" } }
              },
              "example": { "apiKey": "YOUR_API_KEY" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The account's properties.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "properties": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "propertyId": { "type": "string" },
                          "name": { "type": "string" },
                          "createdAt": { "type": "string" },
                          "photoCount": { "type": "integer" },
                          "appUrl": { "type": "string" }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "properties": [
                    { "propertyId": "a1b2c3d4", "name": "Calle Mayor 12", "createdAt": "2026-06-30T10:00:00.000Z", "photoCount": 8, "appUrl": "https://app.pedra.ai/?projectId=a1b2c3d4" }
                  ]
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/InvalidApiKey" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    },
    "/api/list_property_images": {
      "post": {
        "tags": ["Properties"],
        "summary": "List a property's photos",
        "description": "Returns a property's photos as public img.pedra.ai URLs — ready to pass to /api/create_video or the image-editing endpoints.",
        "operationId": "listPropertyImages",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey", "propertyId"],
                "properties": {
                  "apiKey": { "$ref": "#/components/schemas/ApiKey" },
                  "propertyId": { "type": "string", "description": "The property's id (from /api/list_properties)." }
                }
              },
              "example": { "apiKey": "YOUR_API_KEY", "propertyId": "a1b2c3d4" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The property's photos.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "propertyId": { "type": "string" },
                    "name": { "type": "string" },
                    "images": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "imageId": { "type": "string" },
                          "url": { "type": "string" },
                          "name": { "type": "string" },
                          "aspectRatio": { "type": "number" }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "propertyId": "a1b2c3d4",
                  "name": "Calle Mayor 12",
                  "images": [{ "imageId": "e5f6", "url": "https://img.pedra.ai/e5f6", "name": "Image 1", "aspectRatio": 1.5 }]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/InvalidApiKey" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    },
    "/api/create_property": {
      "post": {
        "tags": ["Properties"],
        "summary": "Create a property",
        "description": "Creates an empty property and returns its propertyId and an appUrl (open it in Pedra to upload brand-new local photos).",
        "operationId": "createProperty",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey"],
                "properties": {
                  "apiKey": { "$ref": "#/components/schemas/ApiKey" },
                  "name": { "type": "string", "description": "Property name, e.g. the listing address." }
                }
              },
              "example": { "apiKey": "YOUR_API_KEY", "name": "Calle Mayor 12" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Property created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": { "type": "string" },
                    "propertyId": { "type": "string" },
                    "appUrl": { "type": "string" }
                  }
                },
                "example": { "message": "Property created", "propertyId": "a1b2c3d4", "appUrl": "https://app.pedra.ai/?projectId=a1b2c3d4" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/InvalidApiKey" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    },
    "/api/add_images_to_property": {
      "post": {
        "tags": ["Properties"],
        "summary": "Add photos to a property by URL",
        "description": "Adds photos to a property by URL — the server fetches each URL and stores it (any public https image, or a small data: URI), then returns the stored img.pedra.ai URLs. Up to 20 per call.",
        "operationId": "addImagesToProperty",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey", "propertyId", "imageUrls"],
                "properties": {
                  "apiKey": { "$ref": "#/components/schemas/ApiKey" },
                  "propertyId": { "type": "string" },
                  "imageUrls": {
                    "type": "array",
                    "items": { "type": "string" },
                    "description": "Up to 20 image URLs to fetch and add."
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "propertyId": "a1b2c3d4",
                "imageUrls": ["https://example.com/kitchen.jpg", "https://example.com/living-room.jpg"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Photos added.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": { "type": "string" },
                    "propertyId": { "type": "string" },
                    "added": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "imageId": { "type": "string" },
                          "url": { "type": "string" },
                          "aspectRatio": { "type": "number" }
                        }
                      }
                    },
                    "failed": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": { "url": { "type": "string" }, "error": { "type": "string" } }
                      }
                    },
                    "appUrl": { "type": "string" }
                  }
                },
                "example": {
                  "message": "Added 2 image(s)",
                  "propertyId": "a1b2c3d4",
                  "added": [{ "imageId": "e5f6", "url": "https://img.pedra.ai/e5f6", "aspectRatio": 1.5 }],
                  "failed": [],
                  "appUrl": "https://app.pedra.ai/?projectId=a1b2c3d4"
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/InvalidApiKey" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    },
    "/api/credits": {
      "post": {
        "tags": ["Account"],
        "summary": "Check remaining credits",
        "description": "Returns the account plan and remaining credit balance. Read-only — never deducts credits. Also available as `GET /api/credits?apiKey=YOUR_API_KEY`.",
        "operationId": "credits",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey"],
                "properties": {
                  "apiKey": { "$ref": "#/components/schemas/ApiKey" }
                }
              },
              "example": { "apiKey": "YOUR_API_KEY" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Current plan and credit balance.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "plan": { "type": "string", "description": "Account plan.", "examples": ["pro"] },
                    "creditsRemaining": { "type": "integer", "description": "Credits available for generations." }
                  }
                },
                "example": { "plan": "pro", "creditsRemaining": 4820 }
              }
            }
          },
          "404": { "$ref": "#/components/responses/InvalidApiKey" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    },
    "/api/feedback": {
      "post": {
        "tags": ["Account"],
        "summary": "Vote on a generated image and optionally request credit back",
        "description": "Submits a thumbs up/down on a previously generated image and, on a down vote, can request a credit refund for eligible generations. Free — voting never costs credits. Credit back is eligible only for empty_room, remove_object, edit_via_prompt, renovation, and (non-high-creativity) furnish generations requested within 2 hours.",
        "operationId": "feedback",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey", "vote"],
                "description": "Provide either `imageUrl` or `imageId` to identify the generation.",
                "properties": {
                  "apiKey": { "$ref": "#/components/schemas/ApiKey" },
                  "imageUrl": {
                    "type": "string",
                    "description": "URL of the generated image (as returned by a generation endpoint). Provide this or `imageId`."
                  },
                  "imageId": {
                    "type": "string",
                    "description": "Bare id of the generated image. Provide this or `imageUrl`."
                  },
                  "vote": {
                    "type": "string",
                    "enum": ["up", "down", ""],
                    "description": "\"up\" or \"down\". An empty string clears a previous vote."
                  },
                  "comment": {
                    "type": "string",
                    "description": "Optional free-text explanation of the vote."
                  },
                  "creditBack": {
                    "type": "boolean",
                    "default": false,
                    "description": "On a down vote, request a credit refund for the generation if eligible."
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "imageUrl": "https://img.pedra.ai/a1b2c3d4",
                "vote": "down",
                "comment": "Furniture looked unrealistic",
                "creditBack": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Vote recorded. Includes a `creditBack` object only when a refund was requested.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/FeedbackResult" },
                "examples": {
                  "simpleVote": {
                    "summary": "Vote without credit back",
                    "value": { "ok": true, "vote": "negative" }
                  },
                  "withCreditBack": {
                    "summary": "Down vote with approved credit back",
                    "value": { "ok": true, "vote": "negative", "creditBack": { "status": "approved", "creditsRefunded": 2, "reason": "Eligible generation within the refund window." } }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/InvalidApiKey" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ApiKey": {
        "type": "string",
        "description": "Account API key, sent in the request body. Get one at https://app.pedra.ai under Settings → API."
      },
      "ImageInput": {
        "type": "string",
        "description": "A publicly accessible HTTPS image URL or a base64 data URI (`data:image/png;base64,...`). Remote URLs are fetched within ~20s; use base64 for auth-walled or short-lived presigned URLs.",
        "examples": ["https://example.com/photo.jpg"]
      },
      "PropertyId": {
        "type": "string",
        "description": "Optional id of the property (from /api/list_properties or /api/create_property) this photo/video belongs to. When set, the result is saved into that property's gallery (or, for create_video, its Videos tab) — visible and editable from the app — instead of only being returned as a URL."
      },
      "RoomType": {
        "type": "string",
        "description": "Predefined room type (\"Living room\", \"Dining + Living room\", \"Bedroom\", \"Dining room\", \"Terrace\", \"Entrance\", \"Office\", \"Bathroom\") or any custom string (e.g. \"Kid's room\", \"Home gym\").",
        "examples": ["Bedroom"]
      },
      "Style": {
        "type": "string",
        "description": "A predefined style name (\"Minimalist\", \"Modern\", \"Scandinavian\", \"Industrial\", \"Bohemian\", \"Mid-century modern\", \"Traditional\", \"Mediterranean\", \"Coastal\", \"Rustic\", \"Farmhouse\", \"Contemporary\") or a public URL to a reference image to match.",
        "examples": ["Minimalist"]
      },
      "Creativity": {
        "type": "string",
        "default": "Medium",
        "enum": ["Medium", "High"],
        "description": "Generation creativity. Medium stays closer to the source (2 credits); High allows more variation (1 credit)."
      },
      "VideoFrame": {
        "type": "object",
        "required": ["imageUrl", "effect"],
        "properties": {
          "imageUrl": { "$ref": "#/components/schemas/ImageInput" },
          "effect": {
            "type": "string",
            "enum": ["zoom-in", "zoom-out", "transition", "static"],
            "description": "Frame animation. \"transition\" morphs into `secondImageUrl`; \"static\" is a still frame (0 credits)."
          },
          "secondImageUrl": {
            "allOf": [{ "$ref": "#/components/schemas/ImageInput" }],
            "description": "Target image for the \"transition\" effect. Required when `effect` is \"transition\"."
          },
          "subtitle": { "type": "string", "description": "Caption shown over the frame." },
          "title": { "type": "string", "description": "Large title overlay on the frame, e.g. \"Living Room\"." },
          "watermark": { "$ref": "#/components/schemas/Watermark" },
          "characteristics": {
            "type": "object",
            "description": "Per-frame toggle for the property-characteristics overlay.",
            "properties": {
              "enabled": { "type": "boolean", "description": "Show the `propertyCharacteristics` overlay on this frame." }
            }
          }
        }
      },
      "Watermark": {
        "type": "object",
        "description": "Per-frame watermark settings.",
        "properties": {
          "enabled": { "type": "boolean", "default": true, "description": "Show a watermark on this frame." },
          "position": {
            "type": "string",
            "default": "bottom-right",
            "enum": ["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right"],
            "description": "Watermark placement."
          },
          "opacity": { "type": "number", "default": 1.0, "minimum": 0.0, "maximum": 1.0, "description": "Watermark opacity, 0.0–1.0." }
        }
      },
      "PropertyCharacteristic": {
        "type": "object",
        "required": ["label", "value"],
        "properties": {
          "label": { "type": "string", "description": "Characteristic name. Built-in labels (Bedrooms, Bathrooms, Surface, Price, Location, Parking, Heating, Outdoor) render with an icon.", "examples": ["Price"] },
          "value": { "type": "string", "examples": ["€450,000"] }
        }
      },
      "Music": {
        "type": "object",
        "required": ["enabled"],
        "description": "Background music settings.",
        "properties": {
          "enabled": { "type": "boolean" },
          "track": {
            "type": "string",
            "enum": ["acoustic", "chill", "cinematic", "electronic", "upbeat"],
            "description": "Music genre. One of the keys from GET /api/music_library. Required when `enabled` is true. Legacy values (calm, uplifting, corporate, piano) are still accepted."
          }
        }
      },
      "Voice": {
        "type": "object",
        "required": ["enabled"],
        "description": "Voiceover settings. Generate the audioId with POST /api/generate_voice.",
        "properties": {
          "enabled": { "type": "boolean" },
          "audioId": { "type": "string", "description": "Id of a voiceover from /api/generate_voice. Required when `enabled` is true. Attaches the narration and drives synced subtitles." },
          "audioUrl": { "type": "string", "description": "Legacy alias for audioId." },
          "showSubtitles": { "type": "boolean", "default": true, "description": "Burn in word-synced subtitles from the voiceover." }
        }
      },
      "Branding": {
        "type": "object",
        "description": "Agency/agent branding applied to the video.",
        "properties": {
          "showWatermark": { "type": "boolean", "description": "Use a custom logo as the watermark." },
          "watermarkUrl": { "type": "string", "description": "URL to the logo (PNG with transparency recommended)." },
          "showProfessionalPicture": { "type": "boolean", "description": "Show the agent photo on the ending card." },
          "professionalPictureUrl": { "type": "string", "description": "URL to the agent headshot." },
          "primaryColor": { "type": "string", "description": "Hex color for overlays. Defaults to white (#ffffff).", "default": "#ffffff", "examples": ["#007BFF"] }
        }
      },
      "ImageArrayResult": {
        "type": "object",
        "description": "Success response whose `output` is an array of result images.",
        "properties": {
          "message": { "type": "string", "examples": ["Success"] },
          "output": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "url": { "type": "string", "description": "URL of a generated image.", "examples": ["https://img.pedra.ai/generated-id"] }
              }
            }
          }
        }
      },
      "ImageObjectResult": {
        "type": "object",
        "description": "Success response whose `output` is a single result image.",
        "properties": {
          "message": { "type": "string", "examples": ["Success"] },
          "output": {
            "type": "object",
            "properties": {
              "url": { "type": "string", "description": "URL of the generated image.", "examples": ["https://img.pedra.ai/generated-id"] }
            }
          }
        }
      },
      "VideoResult": {
        "type": "object",
        "properties": {
          "message": { "type": "string", "examples": ["Video created successfully"] },
          "videoId": { "type": "string", "description": "Identifier of the rendered video." },
          "videoUrl": { "type": "string", "description": "URL of the rendered video." }
        }
      },
      "FeedbackResult": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean" },
          "vote": {
            "type": ["string", "null"],
            "description": "Recorded vote: \"positive\", \"negative\", or null when cleared.",
            "examples": ["negative"]
          },
          "creditBack": {
            "type": "object",
            "description": "Present only when a credit refund was requested.",
            "properties": {
              "status": { "type": "string", "enum": ["approved", "rejected", "ineligible"] },
              "creditsRefunded": { "type": "integer" },
              "reason": { "type": "string" }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string", "description": "Human-readable error description." }
        }
      }
    },
    "responses": {
      "ImageArrayResult": {
        "description": "Success. `output` is an array of generated images.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ImageArrayResult" },
            "example": { "message": "Success", "output": [{ "url": "https://img.pedra.ai/generated-id" }] }
          }
        }
      },
      "ImageObjectResult": {
        "description": "Success. `output` is a single generated image.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ImageObjectResult" },
            "example": { "message": "Success", "output": { "url": "https://img.pedra.ai/generated-id" } }
          }
        }
      },
      "BadRequest": {
        "description": "Invalid request — bad or unreachable image URL, missing required field, or validation error.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "Invalid imageUrl" }
          }
        }
      },
      "InsufficientCredits": {
        "description": "Not enough credits for the requested generation.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "Insufficient credits" }
          }
        }
      },
      "InvalidApiKey": {
        "description": "Missing, invalid, or revoked API key.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "User not found" }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "Wrong HTTP method — all endpoints accept POST (credits also accepts GET).",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "Method not allowed" }
          }
        }
      }
    }
  }
}
