{
  "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, listing-video generation, and 360° virtual tours.\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. An AI agent can also get a key for the person it works for, with their approval by email: `POST /api/agent_signup`, then poll `POST /api/agent_signup_status` (neither needs an `apiKey`).\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- The one exception is virtual tours: `create_virtual_tour` and `add_virtual_tour_scenes` return a `tourId` immediately with `status: \"processing\"`; poll `get_virtual_tour` until `status` is `\"ready\"` or `\"failed\"`.\n- Errors are returned with a non-200 status and a JSON body of the shape `{ \"error\": \"...\" }`. Running out of credits returns `403` on the image, video and voice endpoints and `402` (`code: \"insufficient_credits\"`) on the virtual tour endpoints.\n- The social endpoints (`/api/social/*`) are Pedra Pro only (`403` with `code: \"pro_required\"` otherwise), charge no credits, and allow 25 posts per network per rolling 24 hours (`429` with `code: \"daily_limit\"`). Their errors always carry a `code`.\n- There are no per-second rate limits. Images stored from outside the app (add_images_to_property, URL scenes of a tour, upload-link files) count toward a daily upload limit — 30/day on the free plan, 500/day on paid plans, reset at midnight UTC — and a call over it returns `429` with `code: \"upload_limit\"` and stores nothing.\n\n## Versioning and change policy\nThis document is the contract. `info.version` tracks it, and the current spec is\nalways at https://pedra.ai/openapi.json. The API is unversioned in the URL path\ntoday: changes so far have been additive (new endpoints and new optional fields),\nand shipped changes are published at https://pedra.ai/releases. There is no\n`Sunset`/`Deprecation` header scheme yet — treat the spec as authoritative and\nre-fetch it rather than caching field lists indefinitely.\n\n## Scopes\nThe REST API is not scoped: an API key grants access to every endpoint on the\naccount that owns it, so there is no least-privilege subset to request. The\nremote MCP server is scoped, and publishes RFC 9728 protected-resource metadata\nat https://app.pedra.ai/.well-known/oauth-protected-resource.\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."
    },
    {
      "name": "Properties",
      "description": "Browse a Pedra account's properties, read their photo URLs, create properties and add photos (or 360° photos) by URL."
    },
    {
      "name": "Virtual tours",
      "description": "Build hosted, shareable 360° virtual tours from 360° photos. AI names the rooms and places the navigation points. Asynchronous: create, then poll get_virtual_tour."
    },
    {
      "name": "Agent signup",
      "description": "Let an AI agent get an API key for the person it works for — creating their Pedra account if needed — after they approve by email. No apiKey required. ChatGPT and Claude connectors don't need this: they use OAuth."
    },
    {
      "name": "Social",
      "description": "Post or schedule a video on Instagram, Facebook, TikTok and YouTube. Pedra Pro only, no credits. Accounts are connected in the app under Settings › Socials (OAuth), not via the API."
    }
  ],
  "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."
                  },
                  "preserveAspectRatio": {
                    "type": "boolean",
                    "default": false,
                    "description": "By default, the output comes back at whatever size the AI model produces, which may not match the aspect ratio or resolution of your photo. Set to true to get the result at the exact width and height of the input image: it is center-cropped to the original aspect ratio (never stretched) and resized to the original resolution."
                  },
                  "propertyId": {
                    "$ref": "#/components/schemas/PropertyId"
                  },
                  "name": {
                    "$ref": "#/components/schemas/SourceName"
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "imageUrl": "https://example.com/listing-photo.jpg",
                "highFidelity": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success. `output` is an array with exactly one generated image.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageArrayResult"
                },
                "example": {
                  "message": "Success",
                  "output": [
                    {
                      "url": "https://img.pedra.ai/generated-id"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "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"
                }
              }
            }
          },
          "403": {
            "description": "Not enough credits for the requested generation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Insufficient credits"
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "User not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/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."
                  },
                  "preserveAspectRatio": {
                    "type": "boolean",
                    "default": false,
                    "description": "By default, the output comes back at whatever size the AI model produces, which may not match the aspect ratio or resolution of your photo. Set to true to get the result at the exact width and height of the input image: it is center-cropped to the original aspect ratio (never stretched) and resized to the original resolution."
                  },
                  "propertyId": {
                    "$ref": "#/components/schemas/PropertyId"
                  },
                  "name": {
                    "$ref": "#/components/schemas/SourceName"
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "imageUrl": "https://example.com/listing-photo.jpg"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success. `output` is an array with exactly one generated image.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageArrayResult"
                },
                "example": {
                  "message": "Success",
                  "output": [
                    {
                      "url": "https://img.pedra.ai/generated-id"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "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"
                }
              }
            }
          },
          "403": {
            "description": "Not enough credits for the requested generation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Insufficient credits"
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "User not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/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."
                  },
                  "preserveAspectRatio": {
                    "type": "boolean",
                    "default": false,
                    "description": "By default, the output comes back at whatever size the AI model produces, which may not match the aspect ratio or resolution of your photo. Set to true to get the result at the exact width and height of the input image: it is center-cropped to the original aspect ratio (never stretched) and resized to the original resolution."
                  },
                  "propertyId": {
                    "$ref": "#/components/schemas/PropertyId"
                  },
                  "name": {
                    "$ref": "#/components/schemas/SourceName"
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "imageUrl": "https://example.com/house-exterior.jpg",
                "skyStyle": "sunny"
              }
            }
          }
        },
        "responses": {
          "200": {
            "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"
                  }
                }
              }
            }
          },
          "400": {
            "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"
                }
              }
            }
          },
          "403": {
            "description": "Not enough credits for the requested generation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Insufficient credits"
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "User not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/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"
                    ]
                  },
                  "preserveAspectRatio": {
                    "type": "boolean",
                    "default": false,
                    "description": "By default, the output comes back at whatever size the AI model produces, which may not match the aspect ratio or resolution of your photo. Set to true to get the result at the exact width and height of the input image: it is center-cropped to the original aspect ratio (never stretched) and resized to the original resolution."
                  },
                  "propertyId": {
                    "$ref": "#/components/schemas/PropertyId"
                  },
                  "name": {
                    "$ref": "#/components/schemas/SourceName"
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "imageUrl": "https://example.com/street-view.jpg",
                "objectsToBlur": "faces, license plates"
              }
            }
          }
        },
        "responses": {
          "200": {
            "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"
                  }
                }
              }
            }
          },
          "400": {
            "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"
                }
              }
            }
          },
          "403": {
            "description": "Not enough credits for the requested generation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Insufficient credits"
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "User not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/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"
                  },
                  "preserveAspectRatio": {
                    "type": "boolean",
                    "default": false,
                    "description": "By default, the output comes back at whatever size the AI model produces, which may not match the aspect ratio or resolution of your photo. Set to true to get the result at the exact width and height of the input image: it is center-cropped to the original aspect ratio (never stretched) and resized to the original resolution."
                  },
                  "propertyId": {
                    "$ref": "#/components/schemas/PropertyId"
                  },
                  "name": {
                    "$ref": "#/components/schemas/SourceName"
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "imageUrl": "https://example.com/furnished-room.jpg"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success. `output` is an array with exactly one generated image.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageArrayResult"
                },
                "example": {
                  "message": "Success",
                  "output": [
                    {
                      "url": "https://img.pedra.ai/generated-id"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "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"
                }
              }
            }
          },
          "403": {
            "description": "Not enough credits for the requested generation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Insufficient credits"
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "User not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/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"
                  },
                  "preserveAspectRatio": {
                    "type": "boolean",
                    "default": false,
                    "description": "By default, the output comes back at whatever size the AI model produces, which may not match the aspect ratio or resolution of your photo. Set to true to get the result at the exact width and height of the input image: it is center-cropped to the original aspect ratio (never stretched) and resized to the original resolution."
                  },
                  "propertyId": {
                    "$ref": "#/components/schemas/PropertyId"
                  },
                  "name": {
                    "$ref": "#/components/schemas/SourceName"
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "imageUrl": "https://example.com/empty-bedroom.jpg",
                "roomType": "Bedroom",
                "style": "Minimalist",
                "creativity": "Medium"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success. `output` is an array with exactly one generated image.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageArrayResult"
                },
                "example": {
                  "message": "Success",
                  "output": [
                    {
                      "url": "https://img.pedra.ai/generated-id"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "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"
                }
              }
            }
          },
          "403": {
            "description": "Not enough credits for the requested generation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Insufficient credits"
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "User not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/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"
                  },
                  "preserveAspectRatio": {
                    "type": "boolean",
                    "default": false,
                    "description": "By default, the output comes back at whatever size the AI model produces, which may not match the aspect ratio or resolution of your photo. Set to true to get the result at the exact width and height of the input image: it is center-cropped to the original aspect ratio (never stretched) and resized to the original resolution."
                  },
                  "propertyId": {
                    "$ref": "#/components/schemas/PropertyId"
                  },
                  "name": {
                    "$ref": "#/components/schemas/SourceName"
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "imageUrl": "https://example.com/dated-kitchen.jpg",
                "style": "Modern",
                "preserveWindows": true,
                "furnish": false,
                "creativity": "Medium"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success. `output` is an array with exactly one generated image.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageArrayResult"
                },
                "example": {
                  "message": "Success",
                  "output": [
                    {
                      "url": "https://img.pedra.ai/generated-id"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "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"
                }
              }
            }
          },
          "403": {
            "description": "Not enough credits for the requested generation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Insufficient credits"
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "User not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/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"
                    ]
                  },
                  "preserveAspectRatio": {
                    "type": "boolean",
                    "default": false,
                    "description": "By default, the output comes back at whatever size the AI model produces, which may not match the aspect ratio or resolution of your photo. Set to true to get the result at the exact width and height of the input image: it is center-cropped to the original aspect ratio (never stretched) and resized to the original resolution."
                  },
                  "propertyId": {
                    "$ref": "#/components/schemas/PropertyId"
                  },
                  "name": {
                    "$ref": "#/components/schemas/SourceName"
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "imageUrl": "https://example.com/living-room.jpg",
                "prompt": "Repaint the walls white and remove the rug"
              }
            }
          }
        },
        "responses": {
          "200": {
            "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"
                  }
                }
              }
            }
          },
          "400": {
            "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"
                }
              }
            }
          },
          "403": {
            "description": "Not enough credits for the requested generation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Insufficient credits"
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "User not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/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."
                  },
                  "preserveAspectRatio": {
                    "type": "boolean",
                    "default": false,
                    "description": "By default, the output comes back at whatever size the AI model produces, which may not match the aspect ratio or resolution of your photo. Set to true to get the result at the exact width and height of the input image: it is center-cropped to the original aspect ratio (never stretched) and resized to the original resolution."
                  },
                  "propertyId": {
                    "$ref": "#/components/schemas/PropertyId"
                  },
                  "name": {
                    "$ref": "#/components/schemas/SourceName"
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "imageUrl": "https://example.com/room.jpg",
                "maskUrl": "https://example.com/room-mask.png"
              }
            }
          }
        },
        "responses": {
          "200": {
            "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"
                  }
                }
              }
            }
          },
          "400": {
            "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"
                }
              }
            }
          },
          "403": {
            "description": "Not enough credits for the requested generation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Insufficient credits"
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "User not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/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 AI frame (0 for static and lite-zoom-in / lite-zoom-out 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": {
            "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"
                }
              }
            }
          },
          "403": {
            "description": "Not enough credits for the requested generation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Insufficient credits"
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "User not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/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 AI 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": {
            "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"
                }
              }
            }
          },
          "403": {
            "description": "Not enough credits for the requested generation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Insufficient credits"
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "User not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/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": {
            "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"
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "User not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/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": {
            "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"
                }
              }
            }
          },
          "403": {
            "description": "Not enough credits for the requested generation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Insufficient credits"
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "User not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/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": {
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/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": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "User not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/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. Pass type \"360\" to list 360° photos instead.",
        "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)."
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "photo",
                      "360"
                    ],
                    "default": "photo",
                    "description": "\"photo\" (default) lists regular photos; \"360\" lists the property's 360° photos (their imageIds are the sceneIds of a virtual tour)."
                  }
                }
              },
              "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": {
            "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"
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "User not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/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": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "User not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/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. With type \"360\", adds 360° photos for a virtual tour (max 10 per call). Images stored from URLs count toward the daily upload limit (30/day free, 500/day paid); over it, 429 `upload_limit` and nothing is stored.",
        "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": "Image URLs (or data: URIs) to fetch and add — up to 20 per call, or 10 with type \"360\"."
                  },
                  "names": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Optional names, one per image in the same order as imageUrls (e.g. the original file names). Returned as `name` by list_property_images and as `source.name` when the photo is edited. Never drawn on the image. Defaults to \"Image 1\", \"Image 2\"..."
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "photo",
                      "360"
                    ],
                    "default": "photo",
                    "description": "\"360\" adds 360° photos for a virtual tour: each is checked to be equirectangular (2:1), stored like the app stores them (JPEG, 4096 px wide; 6144 px above 20 MB), max 80 MB each and max 10 per call. data: URIs are accepted, so local files can be sent base64-encoded (a few per call, under the 50 MB body limit)."
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "propertyId": "a1b2c3d4",
                "imageUrls": [
                  "https://example.com/kitchen.jpg",
                  "https://example.com/living-room.jpg"
                ],
                "names": [
                  "IMG_0412.jpg",
                  "IMG_0413.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"
                          },
                          "name": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "failed": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "url": {
                            "type": "string"
                          },
                          "error": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "appUrl": {
                      "type": "string"
                    },
                    "type": {
                      "type": "string",
                      "enum": [
                        "360"
                      ],
                      "description": "Present (\"360\") when 360° photos were added."
                    }
                  }
                },
                "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": {
            "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"
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "User not found"
                }
              }
            }
          },
          "429": {
            "description": "Daily limit reached. Nothing is stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadLimitError"
                },
                "example": {
                  "error": "Daily upload limit reached (30 images per day on this plan). It resets at midnight UTC. Paid plans have a higher limit: https://www.pedra.ai/pricing",
                  "code": "upload_limit"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/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": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "User not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/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": {
            "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"
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "User not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/api/create_virtual_tour": {
      "post": {
        "tags": [
          "Virtual tours"
        ],
        "summary": "Create a 360° virtual tour from 360° photos",
        "description": "Imports the 360° photos into a property (a new one unless propertyId is given), then — asynchronously — names unnamed rooms with AI (free) and places navigation links. Returns immediately with status \"processing\"; poll /api/get_virtual_tour until status is \"ready\" or \"failed\" (~10 s per linked room). Credits depend on `linking`: sequential = max(3, ceil(rooms/3)); smart = by room count (≤6: 5, ≤10: 12, ≤15: 25, ≤20: 50, ≤25: 70, ≤30: 100, ≤35: 130, ≤40: 160); none = free. Charged only when linking starts; failed builds cost nothing. One tour per property. Images stored from URLs count toward the daily upload limit (30/day free, 500/day paid); over it, 429 `upload_limit` and nothing is stored.",
        "operationId": "createVirtualTour",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "apiKey"
                ],
                "properties": {
                  "apiKey": {
                    "$ref": "#/components/schemas/ApiKey"
                  },
                  "scenes": {
                    "type": "array",
                    "maxItems": 50,
                    "items": {
                      "$ref": "#/components/schemas/TourSceneInput"
                    },
                    "description": "The 360° photos, in walking order (the first is where the tour opens). Required unless propertyId is given, in which case omitting scenes uses all of the property's 360° photos in upload order."
                  },
                  "imageUrls": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Shorthand for scenes: a plain list of 360° photo URLs."
                  },
                  "propertyId": {
                    "type": "string",
                    "description": "Build the tour in this property. Required when scenes use imageId. Omit to create a new property named after `name`."
                  },
                  "name": {
                    "type": "string",
                    "description": "Tour title (and the new property's name when one is created)."
                  },
                  "linking": {
                    "type": "string",
                    "enum": [
                      "sequential",
                      "smart",
                      "none"
                    ],
                    "default": "sequential",
                    "description": "sequential: each room linked to the next, both directions (pass rooms in walking order). smart: AI links rooms that visibly connect, up to 40 rooms. none: no navigation points."
                  },
                  "language": {
                    "$ref": "#/components/schemas/TourLanguage"
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "name": "Calle Mayor 12",
                "scenes": [
                  {
                    "imageUrl": "https://example.com/360/entrance.jpg",
                    "name": "Entrance"
                  },
                  {
                    "imageUrl": "https://example.com/360/living-room.jpg"
                  },
                  {
                    "imageUrl": "https://example.com/360/kitchen.jpg"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tour created; building in the background.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/VirtualTourSummary"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "scenes": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "sceneId": {
                                "type": "string"
                              },
                              "name": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "source": {
                                "type": "string",
                                "description": "The URL, \"data: URI\", or \"property\" for imageId scenes."
                              }
                            }
                          }
                        },
                        "linking": {
                          "type": "string"
                        },
                        "creditsCost": {
                          "type": "integer"
                        },
                        "estimatedSeconds": {
                          "type": "integer"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "tourId": "1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19",
                  "propertyId": "196e742a-5b0d-4c9e-8a3f-7e2b1c6d9f04",
                  "name": "Calle Mayor 12",
                  "status": "processing",
                  "tourUrl": "https://app.pedra.ai/virtual-tour/1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19",
                  "embedCode": "<iframe src=\"https://app.pedra.ai/virtual-tour/1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19\" title=\"Calle Mayor 12\" width=\"100%\" height=\"600\" style=\"border:0\" allow=\"fullscreen; gyroscope; accelerometer\" allowfullscreen loading=\"lazy\"></iframe>",
                  "appUrl": "https://app.pedra.ai/?projectId=196e742a-5b0d-4c9e-8a3f-7e2b1c6d9f04",
                  "shareable": true,
                  "sceneCount": 4,
                  "linkCount": 0,
                  "coverImageUrl": null,
                  "createdAt": "2026-09-30T15:22:52.028Z",
                  "updatedAt": "2026-09-30T15:22:52.028Z",
                  "progress": {
                    "stage": "queued"
                  },
                  "scenes": [
                    {
                      "sceneId": "f1815046-2a7c-4e3b-9d15-6b8e0c4a7f21",
                      "name": "Entrance",
                      "source": "https://example.com/360/entrance.jpg"
                    }
                  ],
                  "linking": "sequential",
                  "creditsCost": 3,
                  "estimatedSeconds": 47,
                  "message": "Building the tour. Poll get_virtual_tour with this tourId until status is \"ready\" (or \"failed\")."
                }
              }
            }
          },
          "400": {
            "description": "Invalid request — e.g. a photo that isn't a 2:1 360° photo, more than 50 scenes, or an unknown enum value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "'linking' must be one of: sequential, smart, none"
                }
              }
            }
          },
          "402": {
            "description": "Not enough credits. Nothing is created or charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "This tour needs 12 credits and the account has 4.",
                  "code": "insufficient_credits",
                  "creditsRequired": 12,
                  "creditsAvailable": 4,
                  "upgradeUrl": "https://www.pedra.ai/pricing"
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key, or the propertyId isn't on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "Property not found for this account"
                }
              }
            }
          },
          "409": {
            "description": "The property already has a virtual tour (its tourId is returned).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "This property already has a virtual tour. Add rooms with add_virtual_tour_scenes, change it with update_virtual_tour, or delete it with delete_virtual_tour to start over.",
                  "code": "tour_exists",
                  "tourId": "1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19"
                }
              }
            }
          },
          "429": {
            "description": "Daily limit reached. Nothing is stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadLimitError"
                },
                "example": {
                  "error": "Daily upload limit reached (30 images per day on this plan). It resets at midnight UTC. Paid plans have a higher limit: https://www.pedra.ai/pricing",
                  "code": "upload_limit"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "Something went wrong on our side. Please try again."
                }
              }
            }
          }
        }
      }
    },
    "/api/get_virtual_tour": {
      "post": {
        "tags": [
          "Virtual tours"
        ],
        "summary": "Get a virtual tour (poll this)",
        "description": "Returns the tour's status (processing | ready | failed) and, when ready, its scenes, navigation links and display settings. Poll it after create_virtual_tour and add_virtual_tour_scenes. Free.",
        "operationId": "getVirtualTour",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "apiKey",
                  "tourId"
                ],
                "properties": {
                  "apiKey": {
                    "$ref": "#/components/schemas/ApiKey"
                  },
                  "tourId": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "tourId": "1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tour.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VirtualTour"
                },
                "example": {
                  "tourId": "1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19",
                  "propertyId": "196e742a-5b0d-4c9e-8a3f-7e2b1c6d9f04",
                  "name": "Calle Mayor 12",
                  "status": "ready",
                  "tourUrl": "https://app.pedra.ai/virtual-tour/1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19",
                  "embedCode": "<iframe src=\"https://app.pedra.ai/virtual-tour/1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19\" title=\"Calle Mayor 12\" width=\"100%\" height=\"600\" style=\"border:0\" allow=\"fullscreen; gyroscope; accelerometer\" allowfullscreen loading=\"lazy\"></iframe>",
                  "appUrl": "https://app.pedra.ai/?projectId=196e742a-5b0d-4c9e-8a3f-7e2b1c6d9f04",
                  "shareable": true,
                  "sceneCount": 4,
                  "linkCount": 6,
                  "coverImageUrl": "https://img.pedra.ai/f1815046-2a7c-4e3b-9d15-6b8e0c4a7f21",
                  "createdAt": "2026-09-30T15:22:52.028Z",
                  "updatedAt": "2026-09-30T15:23:41.310Z",
                  "scenes": [
                    {
                      "sceneId": "f1815046-2a7c-4e3b-9d15-6b8e0c4a7f21",
                      "name": "Entrance",
                      "imageUrl": "https://img.pedra.ai/f1815046-2a7c-4e3b-9d15-6b8e0c4a7f21"
                    },
                    {
                      "sceneId": "cf0a13ee-8b4d-4f62-a0c7-3e9d5b1f2a68",
                      "name": "Living room",
                      "imageUrl": "https://img.pedra.ai/cf0a13ee-8b4d-4f62-a0c7-3e9d5b1f2a68"
                    }
                  ],
                  "links": [
                    {
                      "linkId": "ai-7c21",
                      "fromSceneId": "f1815046-2a7c-4e3b-9d15-6b8e0c4a7f21",
                      "toSceneId": "cf0a13ee-8b4d-4f62-a0c7-3e9d5b1f2a68",
                      "yaw": -18,
                      "pitch": 0,
                      "aiGenerated": true
                    },
                    {
                      "linkId": "ai-7c22",
                      "fromSceneId": "cf0a13ee-8b4d-4f62-a0c7-3e9d5b1f2a68",
                      "toSceneId": "f1815046-2a7c-4e3b-9d15-6b8e0c4a7f21",
                      "yaw": 164,
                      "pitch": 0,
                      "aiGenerated": true
                    }
                  ],
                  "settings": {
                    "navigationStyle": "white",
                    "navigationSize": "medium",
                    "showLabels": false,
                    "language": "en"
                  }
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key, or a tour/property that isn't on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "Virtual tour not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "Something went wrong on our side. Please try again."
                }
              }
            }
          }
        }
      }
    },
    "/api/list_virtual_tours": {
      "post": {
        "tags": [
          "Virtual tours"
        ],
        "summary": "List virtual tours",
        "description": "Returns the account's virtual tours, newest first (max 100), with summary fields only (no scenes, links or settings). Deleted tours are excluded. Free.",
        "operationId": "listVirtualTours",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "apiKey"
                ],
                "properties": {
                  "apiKey": {
                    "$ref": "#/components/schemas/ApiKey"
                  },
                  "propertyId": {
                    "type": "string",
                    "description": "Only return this property's tour."
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tours.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tours": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/VirtualTourSummary"
                      }
                    }
                  }
                },
                "example": {
                  "tours": [
                    {
                      "tourId": "1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19",
                      "propertyId": "196e742a-5b0d-4c9e-8a3f-7e2b1c6d9f04",
                      "name": "Calle Mayor 12",
                      "status": "ready",
                      "tourUrl": "https://app.pedra.ai/virtual-tour/1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19",
                      "embedCode": "<iframe src=\"https://app.pedra.ai/virtual-tour/1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19\" title=\"Calle Mayor 12\" width=\"100%\" height=\"600\" style=\"border:0\" allow=\"fullscreen; gyroscope; accelerometer\" allowfullscreen loading=\"lazy\"></iframe>",
                      "appUrl": "https://app.pedra.ai/?projectId=196e742a-5b0d-4c9e-8a3f-7e2b1c6d9f04",
                      "shareable": true,
                      "sceneCount": 4,
                      "linkCount": 6,
                      "coverImageUrl": "https://img.pedra.ai/f1815046-2a7c-4e3b-9d15-6b8e0c4a7f21",
                      "createdAt": "2026-09-30T15:22:52.028Z",
                      "updatedAt": "2026-09-30T15:23:41.310Z"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key, or a tour/property that isn't on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "Virtual tour not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "Something went wrong on our side. Please try again."
                }
              }
            }
          }
        }
      }
    },
    "/api/update_virtual_tour": {
      "post": {
        "tags": [
          "Virtual tours"
        ],
        "summary": "Update a virtual tour",
        "description": "Renames the tour or its rooms, reorders or removes rooms, replaces all navigation links, and changes the navigation point style. Free and instant; validation is all-or-nothing. Returns the full tour. 409 tour_processing while the tour is building.",
        "operationId": "updateVirtualTour",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "apiKey",
                  "tourId"
                ],
                "properties": {
                  "apiKey": {
                    "$ref": "#/components/schemas/ApiKey"
                  },
                  "tourId": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "sceneNames": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "Room names by sceneId."
                  },
                  "sceneOrder": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Every sceneId exactly once; the first is where the tour opens."
                  },
                  "removeScenes": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "sceneIds to remove. Photos stay in the property; links touching them are dropped; at least one scene must remain."
                  },
                  "links": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/TourLink"
                    },
                    "description": "Replaces ALL links. Each link is one direction."
                  },
                  "navigationStyle": {
                    "type": "string",
                    "enum": [
                      "white",
                      "blue"
                    ]
                  },
                  "navigationSize": {
                    "type": "string",
                    "enum": [
                      "small",
                      "medium",
                      "large"
                    ]
                  },
                  "showLabels": {
                    "type": "boolean",
                    "description": "Always show room-name labels on navigation points."
                  },
                  "language": {
                    "$ref": "#/components/schemas/TourLanguage"
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "tourId": "1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19",
                "sceneNames": {
                  "cf0a13ee-8b4d-4f62-a0c7-3e9d5b1f2a68": "Living room"
                },
                "showLabels": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated tour.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VirtualTour"
                },
                "example": {
                  "tourId": "1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19",
                  "propertyId": "196e742a-5b0d-4c9e-8a3f-7e2b1c6d9f04",
                  "name": "Calle Mayor 12",
                  "status": "ready",
                  "tourUrl": "https://app.pedra.ai/virtual-tour/1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19",
                  "embedCode": "<iframe src=\"https://app.pedra.ai/virtual-tour/1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19\" title=\"Calle Mayor 12\" width=\"100%\" height=\"600\" style=\"border:0\" allow=\"fullscreen; gyroscope; accelerometer\" allowfullscreen loading=\"lazy\"></iframe>",
                  "appUrl": "https://app.pedra.ai/?projectId=196e742a-5b0d-4c9e-8a3f-7e2b1c6d9f04",
                  "shareable": true,
                  "sceneCount": 4,
                  "linkCount": 6,
                  "coverImageUrl": "https://img.pedra.ai/f1815046-2a7c-4e3b-9d15-6b8e0c4a7f21",
                  "createdAt": "2026-09-30T15:22:52.028Z",
                  "updatedAt": "2026-09-30T15:23:41.310Z",
                  "scenes": [
                    {
                      "sceneId": "f1815046-2a7c-4e3b-9d15-6b8e0c4a7f21",
                      "name": "Entrance",
                      "imageUrl": "https://img.pedra.ai/f1815046-2a7c-4e3b-9d15-6b8e0c4a7f21"
                    },
                    {
                      "sceneId": "cf0a13ee-8b4d-4f62-a0c7-3e9d5b1f2a68",
                      "name": "Living room",
                      "imageUrl": "https://img.pedra.ai/cf0a13ee-8b4d-4f62-a0c7-3e9d5b1f2a68"
                    }
                  ],
                  "links": [
                    {
                      "linkId": "ai-7c21",
                      "fromSceneId": "f1815046-2a7c-4e3b-9d15-6b8e0c4a7f21",
                      "toSceneId": "cf0a13ee-8b4d-4f62-a0c7-3e9d5b1f2a68",
                      "yaw": -18,
                      "pitch": 0,
                      "aiGenerated": true
                    },
                    {
                      "linkId": "ai-7c22",
                      "fromSceneId": "cf0a13ee-8b4d-4f62-a0c7-3e9d5b1f2a68",
                      "toSceneId": "f1815046-2a7c-4e3b-9d15-6b8e0c4a7f21",
                      "yaw": 164,
                      "pitch": 0,
                      "aiGenerated": true
                    }
                  ],
                  "settings": {
                    "navigationStyle": "white",
                    "navigationSize": "medium",
                    "showLabels": false,
                    "language": "en"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request — e.g. a photo that isn't a 2:1 360° photo, more than 50 scenes, or an unknown enum value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "'linking' must be one of: sequential, smart, none"
                }
              }
            }
          },
          "409": {
            "description": "The tour is still being built.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "The tour is still being built. Try again when get_virtual_tour says it's ready.",
                  "code": "tour_processing"
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key, or a tour/property that isn't on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "Virtual tour not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "Something went wrong on our side. Please try again."
                }
              }
            }
          }
        }
      }
    },
    "/api/add_virtual_tour_scenes": {
      "post": {
        "tags": [
          "Virtual tours"
        ],
        "summary": "Add rooms to a virtual tour",
        "description": "Adds 360° photos to an existing tour. Sequential linking links only the new stretch (last existing room ↔ first new room, then each new room ↔ next); existing links are kept. Costs max(3, ceil(newRooms/3)) credits. Asynchronous: the tour stays live; poll /api/get_virtual_tour until status is \"ready\" again. Images stored from URLs count toward the daily upload limit (30/day free, 500/day paid); over it, 429 `upload_limit` and nothing is stored.",
        "operationId": "addVirtualTourScenes",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "apiKey",
                  "tourId",
                  "scenes"
                ],
                "properties": {
                  "apiKey": {
                    "$ref": "#/components/schemas/ApiKey"
                  },
                  "tourId": {
                    "type": "string"
                  },
                  "scenes": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/TourSceneInput"
                    },
                    "description": "Same shape as create_virtual_tour. imageId scenes must be 360° photos in the tour's property."
                  },
                  "linking": {
                    "type": "string",
                    "enum": [
                      "sequential",
                      "none"
                    ],
                    "default": "sequential"
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "tourId": "1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19",
                "scenes": [
                  {
                    "imageUrl": "https://example.com/360/bathroom.jpg",
                    "name": "Bathroom"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Scenes being added.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/VirtualTourSummary"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "addedScenes": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "sceneId": {
                                "type": "string"
                              },
                              "name": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "source": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "creditsCost": {
                          "type": "integer"
                        },
                        "estimatedSeconds": {
                          "type": "integer"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "tourId": "1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19",
                  "propertyId": "196e742a-5b0d-4c9e-8a3f-7e2b1c6d9f04",
                  "name": "Calle Mayor 12",
                  "status": "processing",
                  "tourUrl": "https://app.pedra.ai/virtual-tour/1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19",
                  "embedCode": "<iframe src=\"https://app.pedra.ai/virtual-tour/1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19\" title=\"Calle Mayor 12\" width=\"100%\" height=\"600\" style=\"border:0\" allow=\"fullscreen; gyroscope; accelerometer\" allowfullscreen loading=\"lazy\"></iframe>",
                  "appUrl": "https://app.pedra.ai/?projectId=196e742a-5b0d-4c9e-8a3f-7e2b1c6d9f04",
                  "shareable": true,
                  "sceneCount": 5,
                  "linkCount": 6,
                  "coverImageUrl": null,
                  "createdAt": "2026-09-30T15:22:52.028Z",
                  "updatedAt": "2026-09-30T15:22:52.028Z",
                  "progress": {
                    "stage": "queued"
                  },
                  "addedScenes": [
                    {
                      "sceneId": "3e7b1a95-4c0f-4b2d-9e68-1a5d8c3f7b02",
                      "name": "Bathroom",
                      "source": "https://example.com/360/bathroom.jpg"
                    }
                  ],
                  "creditsCost": 3,
                  "estimatedSeconds": 21,
                  "message": "Adding the scenes. The tour stays live while this runs. Poll get_virtual_tour until status is \"ready\"."
                }
              }
            }
          },
          "400": {
            "description": "Invalid request. code no_new_scenes when every scene is already in the tour.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "Those scenes are already in the tour",
                  "code": "no_new_scenes"
                }
              }
            }
          },
          "402": {
            "description": "Not enough credits. Nothing is created or charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "This tour needs 12 credits and the account has 4.",
                  "code": "insufficient_credits",
                  "creditsRequired": 12,
                  "creditsAvailable": 4,
                  "upgradeUrl": "https://www.pedra.ai/pricing"
                }
              }
            }
          },
          "409": {
            "description": "The tour is still building (tour_processing) or its build failed (tour_failed).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "The tour is still being built. Try again when get_virtual_tour says it's ready.",
                  "code": "tour_processing"
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key, or a tour/property that isn't on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "Virtual tour not found"
                }
              }
            }
          },
          "429": {
            "description": "Daily limit reached. Nothing is stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadLimitError"
                },
                "example": {
                  "error": "Daily upload limit reached (30 images per day on this plan). It resets at midnight UTC. Paid plans have a higher limit: https://www.pedra.ai/pricing",
                  "code": "upload_limit"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "Something went wrong on our side. Please try again."
                }
              }
            }
          }
        }
      }
    },
    "/api/delete_virtual_tour": {
      "post": {
        "tags": [
          "Virtual tours"
        ],
        "summary": "Delete a virtual tour",
        "description": "Deletes the tour; its public link returns 404 from then on. The 360° photos stay in the property. API only (not exposed as an MCP tool).",
        "operationId": "deleteVirtualTour",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "apiKey",
                  "tourId"
                ],
                "properties": {
                  "apiKey": {
                    "$ref": "#/components/schemas/ApiKey"
                  },
                  "tourId": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "tourId": "1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tour deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "tourId": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "message": "Virtual tour deleted. Its 360° photos are still in the property.",
                  "tourId": "1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19"
                }
              }
            }
          },
          "409": {
            "description": "The tour is still being built.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "The tour is still being built. Try again when get_virtual_tour says it's ready.",
                  "code": "tour_processing"
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key, or a tour/property that isn't on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "Virtual tour not found"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "Something went wrong on our side. Please try again."
                }
              }
            }
          }
        }
      }
    },
    "/api/create_upload_link": {
      "post": {
        "tags": [
          "Virtual tours"
        ],
        "summary": "Create a no-login upload link for photos and 360° photos",
        "description": "Returns a no-login upload page (valid 24 h) that can only add photos to one property. Works on a phone or computer: drag & drop or pick files, up to 100 files, JPEG/PNG/WebP up to 80 MB each (iPhone HEIC photos work when uploaded from the iPhone). `type: \"any\"` (default) takes regular photos and 360° photos — 2:1 images are detected and stored as 360° photos automatically; `type: \"360\"` takes 360° photos only (use it for virtual tours). Photos are stored like the web app stores them (long side 2048 px, 3072 px over 20 MB; 360° photos 4096/6144 px wide). Files are sorted by file name (= shooting order). When the upload is done, call /api/list_property_images (type \"photo\" or \"360\") for URLs to edit, use in create_video, or call /api/create_virtual_tour with the propertyId and no scenes. This is how chat assistants get photos from a user's phone. Free; 5 links/day on the free plan, 50/day on paid plans (429 `upload_link_limit`). Each uploaded file counts toward the daily upload limit.",
        "operationId": "createUploadLink",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "apiKey"
                ],
                "properties": {
                  "apiKey": {
                    "$ref": "#/components/schemas/ApiKey"
                  },
                  "propertyId": {
                    "type": "string",
                    "description": "Property to upload into. Omit to create a new property named after `name`."
                  },
                  "name": {
                    "type": "string"
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "any",
                      "360"
                    ],
                    "default": "any",
                    "description": "\"any\": photos and 360° photos (2:1 images stored as 360° automatically). \"360\": 360° photos only — use it for a virtual tour."
                  },
                  "language": {
                    "$ref": "#/components/schemas/TourLanguage"
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "name": "Calle Mayor 12",
                "type": "any"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Upload link created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "uploadUrl": {
                      "type": "string"
                    },
                    "propertyId": {
                      "type": "string"
                    },
                    "propertyName": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "type": {
                      "type": "string",
                      "enum": [
                        "any",
                        "360"
                      ]
                    },
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "maxFiles": {
                      "type": "integer",
                      "description": "Max files per link (100)."
                    },
                    "appUrl": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "uploadUrl": "https://app.pedra.ai/upload/q7Ht2vXk9LmP4wRz8NcB1sYd6FgJ3eUa0KoV5iTn",
                  "propertyId": "196e742a-5b0d-4c9e-8a3f-7e2b1c6d9f04",
                  "propertyName": "Calle Mayor 12",
                  "type": "any",
                  "expiresAt": "2026-10-01T15:26:30.687Z",
                  "maxFiles": 100,
                  "appUrl": "https://app.pedra.ai/?projectId=196e742a-5b0d-4c9e-8a3f-7e2b1c6d9f04",
                  "message": "Send this link to whoever has the photos (360° photos are recognised automatically). It works on a phone or a computer with no login, for 24 hours. When they're done, list the property's photos (list_property_images; type \"360\" for 360° photos) and use them."
                }
              }
            }
          },
          "400": {
            "description": "Invalid `type`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "'type' must be \"any\" (photos and 360° photos) or \"360\""
                }
              }
            }
          },
          "404": {
            "description": "Missing, invalid, or revoked API key, or a tour/property that isn't on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "Virtual tour not found"
                }
              }
            }
          },
          "429": {
            "description": "Daily limit reached. Nothing is stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadLimitError"
                },
                "example": {
                  "error": "This account can make 5 upload links a day. Reuse one of today's links (each works for 24 hours), or try again tomorrow.",
                  "code": "upload_link_limit"
                }
              }
            }
          },
          "405": {
            "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"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TourError"
                },
                "example": {
                  "error": "Something went wrong on our side. Please try again."
                }
              }
            }
          }
        }
      }
    },
    "/api/social/accounts": {
      "post": {
        "tags": [
          "Social"
        ],
        "summary": "List connected social accounts",
        "operationId": "socialListAccounts",
        "description": "Returns every supported network and whether it's connected for this Pedra user. `account` is the Instagram/TikTok username or the Facebook Page name; YouTube doesn't share the channel name, so it's always null. Accounts are connected in the app under Settings › Socials, not via the API. Pedra Pro only. Free.",
        "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": "Every supported network.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "accounts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "network",
                          "connected",
                          "account"
                        ],
                        "properties": {
                          "network": {
                            "$ref": "#/components/schemas/SocialNetwork"
                          },
                          "connected": {
                            "type": "boolean"
                          },
                          "account": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Username or Page name; always null for YouTube."
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "accounts": [
                    {
                      "network": "instagram",
                      "connected": true,
                      "account": "pedra_ai"
                    },
                    {
                      "network": "facebook",
                      "connected": true,
                      "account": "Pedra AI"
                    },
                    {
                      "network": "tiktok",
                      "connected": false,
                      "account": null
                    },
                    {
                      "network": "youtube",
                      "connected": true,
                      "account": null
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SocialError"
                },
                "example": {
                  "error": "Invalid API key",
                  "code": "invalid_api_key"
                }
              }
            }
          },
          "403": {
            "description": "The account isn't on Pedra Pro.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SocialError"
                },
                "example": {
                  "error": "The Social API is only available on Pedra Pro.",
                  "code": "pro_required"
                }
              }
            }
          },
          "405": {
            "description": "Wrong HTTP method; these endpoints accept POST only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Method not allowed"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Something went wrong on our side. Please try again."
                }
              }
            }
          }
        }
      }
    },
    "/api/social/posts": {
      "post": {
        "tags": [
          "Social"
        ],
        "summary": "Post or schedule a video",
        "operationId": "socialCreatePosts",
        "description": "Posts one video to one or more connected networks, now or at `scheduledAt`. Pedra copies the video to its own storage (max 1 GB); a Pedra video URL (img.pedra.ai) is used as is. Omit `scheduledAt` to post now (goes out within about a minute). Returns one post per network with `status: \"pending\"`; follow up with socialListPosts. Instagram posts as a Reel on a professional account. Video only. Pedra Pro only; a scheduled post is only published if the account is still on Pro when it's due, otherwise it fails. No credits; 25 posts per network per rolling 24 hours.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "apiKey",
                  "videoUrl",
                  "posts"
                ],
                "properties": {
                  "apiKey": {
                    "$ref": "#/components/schemas/ApiKey"
                  },
                  "videoUrl": {
                    "type": "string",
                    "format": "uri",
                    "description": "Public https URL of an MP4 (max 1 GB)."
                  },
                  "scheduledAt": {
                    "type": "string",
                    "format": "date-time",
                    "description": "ISO 8601. Omit to post now. Can't be in the past; max 180 days ahead."
                  },
                  "timezone": {
                    "type": "string",
                    "description": "IANA timezone (e.g. Europe/Madrid), stored for display only."
                  },
                  "coverOffsetMs": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 600000,
                    "description": "Cover frame on Instagram and TikTok, in milliseconds from the start of the video. Ignored for Facebook and YouTube."
                  },
                  "posts": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 4,
                    "description": "One entry per network, each network at most once.",
                    "items": {
                      "$ref": "#/components/schemas/SocialPostInput"
                    }
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "videoUrl": "https://example.com/videos/calle-mayor-12.mp4",
                "scheduledAt": "2026-10-14T10:00:00+02:00",
                "timezone": "Europe/Madrid",
                "coverOffsetMs": 1000,
                "posts": [
                  {
                    "network": "instagram",
                    "caption": "3-bed flat with a sunny terrace on Calle Mayor 12. Book a visit, link in bio."
                  },
                  {
                    "network": "tiktok",
                    "caption": "3-bed flat with a sunny terrace on Calle Mayor 12. Book a visit, link in bio.",
                    "privacyLevel": "PUBLIC_TO_EVERYONE"
                  },
                  {
                    "network": "youtube",
                    "title": "Calle Mayor 12: 3-bed flat with terrace",
                    "caption": "3-bed flat with a sunny terrace on Calle Mayor 12. Book a visit, link in bio.",
                    "privacyStatus": "public"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Posts created, one per network.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "videoId",
                    "videoUrl",
                    "posts"
                  ],
                  "properties": {
                    "videoId": {
                      "type": "string"
                    },
                    "videoUrl": {
                      "type": "string",
                      "description": "The stored copy of the video."
                    },
                    "posts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SocialPost"
                      }
                    }
                  }
                },
                "example": {
                  "videoId": "cf568771-4d2a-4e8b-9c3f-1a7e5b2d8f60",
                  "videoUrl": "https://img.pedra.ai/social-videos/cf568771-4d2a-4e8b-9c3f-1a7e5b2d8f60.mp4",
                  "posts": [
                    {
                      "postId": "KBkxMfAPARdEG4gix",
                      "network": "instagram",
                      "status": "pending",
                      "scheduledAt": "2026-10-14T08:00:00.000Z",
                      "postedAt": null,
                      "platformPostId": null,
                      "videoId": "cf568771-4d2a-4e8b-9c3f-1a7e5b2d8f60",
                      "error": null
                    },
                    {
                      "postId": "q3TzR8wYbN5cLm2Hd",
                      "network": "tiktok",
                      "status": "pending",
                      "scheduledAt": "2026-10-14T08:00:00.000Z",
                      "postedAt": null,
                      "platformPostId": null,
                      "videoId": "cf568771-4d2a-4e8b-9c3f-1a7e5b2d8f60",
                      "error": null
                    },
                    {
                      "postId": "Xe7pGv2sJk9WfA4Ru",
                      "network": "youtube",
                      "status": "pending",
                      "scheduledAt": "2026-10-14T08:00:00.000Z",
                      "postedAt": null,
                      "platformPostId": null,
                      "videoId": "cf568771-4d2a-4e8b-9c3f-1a7e5b2d8f60",
                      "error": null
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid input: invalid_video_url, video_unreachable, not_a_video, video_too_large, invalid_network, not_connected, invalid_post (the message names the network and field), invalid_scheduled_at or invalid_cover.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SocialError"
                },
                "example": {
                  "error": "tiktok: privacyLevel is required (PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR or SELF_ONLY).",
                  "code": "invalid_post"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SocialError"
                },
                "example": {
                  "error": "Invalid API key",
                  "code": "invalid_api_key"
                }
              }
            }
          },
          "403": {
            "description": "The account isn't on Pedra Pro.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SocialError"
                },
                "example": {
                  "error": "The Social API is only available on Pedra Pro.",
                  "code": "pro_required"
                }
              }
            }
          },
          "405": {
            "description": "Wrong HTTP method; these endpoints accept POST only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Method not allowed"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Something went wrong on our side. Please try again."
                }
              }
            }
          },
          "429": {
            "description": "25 posts on that network in the last 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SocialError"
                },
                "example": {
                  "error": "Daily limit reached: 25 instagram posts in the last 24 hours.",
                  "code": "daily_limit"
                }
              }
            }
          }
        }
      }
    },
    "/api/social/posts/list": {
      "post": {
        "tags": [
          "Social"
        ],
        "summary": "List social posts",
        "operationId": "socialListPosts",
        "description": "Returns the account's posts sorted by scheduledAt, newest first, optionally filtered by status, network and a scheduledAt range. Use it to see what went out (`platformPostId`) and why anything failed (`error`). Pedra Pro only. Free.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "apiKey"
                ],
                "properties": {
                  "apiKey": {
                    "$ref": "#/components/schemas/ApiKey"
                  },
                  "status": {
                    "$ref": "#/components/schemas/SocialPostStatus"
                  },
                  "network": {
                    "$ref": "#/components/schemas/SocialNetwork"
                  },
                  "from": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Only posts with scheduledAt at or after this time."
                  },
                  "to": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Only posts with scheduledAt at or before this time."
                  },
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100,
                    "default": 50
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "limit": 20
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The posts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "posts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SocialPost"
                      }
                    }
                  }
                },
                "example": {
                  "posts": [
                    {
                      "postId": "Xe7pGv2sJk9WfA4Ru",
                      "network": "youtube",
                      "status": "posted",
                      "scheduledAt": "2026-10-14T08:00:00.000Z",
                      "postedAt": "2026-10-14T08:00:41.000Z",
                      "platformPostId": "k7Qm2LrT0aE",
                      "videoId": "cf568771-4d2a-4e8b-9c3f-1a7e5b2d8f60",
                      "error": null
                    },
                    {
                      "postId": "q3TzR8wYbN5cLm2Hd",
                      "network": "tiktok",
                      "status": "failed",
                      "scheduledAt": "2026-10-14T08:00:00.000Z",
                      "postedAt": null,
                      "platformPostId": null,
                      "videoId": "cf568771-4d2a-4e8b-9c3f-1a7e5b2d8f60",
                      "error": "TikTok rejected the post: the account was disconnected. Reconnect it under Settings > Socials."
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SocialError"
                },
                "example": {
                  "error": "Invalid API key",
                  "code": "invalid_api_key"
                }
              }
            }
          },
          "403": {
            "description": "The account isn't on Pedra Pro.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SocialError"
                },
                "example": {
                  "error": "The Social API is only available on Pedra Pro.",
                  "code": "pro_required"
                }
              }
            }
          },
          "405": {
            "description": "Wrong HTTP method; these endpoints accept POST only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Method not allowed"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Something went wrong on our side. Please try again."
                }
              }
            }
          }
        }
      }
    },
    "/api/social/posts/cancel": {
      "post": {
        "tags": [
          "Social"
        ],
        "summary": "Cancel a scheduled post",
        "operationId": "socialCancelPost",
        "description": "Cancels a post that hasn't gone out yet. Only pending posts can be cancelled. Other posts from the same call are not affected. Pedra Pro only. Free.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "apiKey",
                  "postId"
                ],
                "properties": {
                  "apiKey": {
                    "$ref": "#/components/schemas/ApiKey"
                  },
                  "postId": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "apiKey": "YOUR_API_KEY",
                "postId": "KBkxMfAPARdEG4gix"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The cancelled post.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "post": {
                      "$ref": "#/components/schemas/SocialPost"
                    }
                  }
                },
                "example": {
                  "post": {
                    "postId": "KBkxMfAPARdEG4gix",
                    "network": "instagram",
                    "status": "cancelled",
                    "scheduledAt": "2026-10-14T08:00:00.000Z",
                    "postedAt": null,
                    "platformPostId": null,
                    "videoId": "cf568771-4d2a-4e8b-9c3f-1a7e5b2d8f60",
                    "error": null
                  }
                }
              }
            }
          },
          "404": {
            "description": "No post with that postId on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SocialError"
                },
                "example": {
                  "error": "Post not found",
                  "code": "post_not_found"
                }
              }
            }
          },
          "409": {
            "description": "The post is already posted, failed, processing or cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SocialError"
                },
                "example": {
                  "error": "Only pending posts can be cancelled. This one is already posted.",
                  "code": "not_cancellable"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SocialError"
                },
                "example": {
                  "error": "Invalid API key",
                  "code": "invalid_api_key"
                }
              }
            }
          },
          "403": {
            "description": "The account isn't on Pedra Pro.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SocialError"
                },
                "example": {
                  "error": "The Social API is only available on Pedra Pro.",
                  "code": "pro_required"
                }
              }
            }
          },
          "405": {
            "description": "Wrong HTTP method; these endpoints accept POST only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Method not allowed"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Something went wrong on our side. Please try again."
                }
              }
            }
          }
        }
      }
    },
    "/api/agent_signup": {
      "post": {
        "tags": [
          "Agent signup"
        ],
        "summary": "Request an API key for a person, by email",
        "description": "No apiKey needed. Emails the person a confirmation link (valid 30 minutes). A new account says what they do (role), picks a language and chooses a password there and clicks \"Create account and allow\"; an existing account clicks \"Allow\"; they can also decline. Nothing is created until they click. Then poll /api/agent_signup_status every `pollAfterSeconds` (5 s) for up to ~30 minutes. The response is the same whether or not the email already has an account. Abuse protections: inbox confirmation required, disposable email domains refused, rate limits (3/hour per email, 10/day per IP, plus a global daily cap). For agents with no browser in the loop (Claude Code, scripts, the local MCP server); ChatGPT and Claude connectors use OAuth instead.",
        "operationId": "agentSignup",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "The email of the person the agent works for."
                  },
                  "agentName": {
                    "type": "string",
                    "maxLength": 60,
                    "description": "Shown in the email and on the confirmation page, e.g. \"Claude Code\"."
                  }
                }
              },
              "example": {
                "email": "maria@example.com",
                "agentName": "Claude Code"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Confirmation email sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "requestId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending"
                      ]
                    },
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "pollAfterSeconds": {
                      "type": "integer"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "requestId": "hT4kP9wZ2mQ7xR1vB8nC3sD6fG0jL5yE",
                  "status": "pending",
                  "expiresAt": "2026-09-30T10:30:00.000Z",
                  "pollAfterSeconds": 5,
                  "message": "We emailed a link to maria@example.com. Ask the person to open it and confirm (new accounts set a password there). Then poll agent_signup_status with this requestId to get the API key."
                }
              }
            }
          },
          "400": {
            "description": "Invalid email address, or a disposable email domain (`code: \"disposable_email\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentSignupError"
                },
                "example": {
                  "error": "Please use a permanent email address.",
                  "code": "disposable_email"
                }
              }
            }
          },
          "405": {
            "description": "Wrong HTTP method.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Method not allowed"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests for this email, this IP, or overall.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentSignupError"
                },
                "example": {
                  "error": "Too many requests for this email address. Check the inbox for the link already sent, or try again in an hour.",
                  "code": "rate_limited"
                }
              }
            }
          },
          "500": {
            "description": "The confirmation email could not be sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Could not send the confirmation email. Please try again."
                }
              }
            }
          },
          "503": {
            "description": "Agent signup is unavailable; the person can sign up at app.pedra.ai and copy a key from Settings → API.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentSignupError"
                },
                "example": {
                  "error": "Agent signup isn't available right now. The person can sign up at https://app.pedra.ai and create an API key in Settings.",
                  "code": "unavailable"
                }
              }
            }
          }
        }
      }
    },
    "/api/agent_signup_status": {
      "post": {
        "tags": [
          "Agent signup"
        ],
        "summary": "Poll an agent signup request",
        "description": "No apiKey needed. Returns `pending` (poll again in 5 s), `approved` (with the apiKey — retrievable for 15 minutes after approval; the key itself doesn't expire), `denied` or `expired`. New accounts start with Pedra's free trial credits. When `creditsRemaining` is 0, `note` says the person can get credits with a plan.",
        "operationId": "agentSignupStatus",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "requestId"
                ],
                "properties": {
                  "requestId": {
                    "type": "string",
                    "description": "From /api/agent_signup."
                  }
                }
              },
              "example": {
                "requestId": "hT4kP9wZ2mQ7xR1vB8nC3sD6fG0jL5yE"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Current status of the request.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "approved",
                        "denied",
                        "expired"
                      ]
                    },
                    "pollAfterSeconds": {
                      "type": "integer",
                      "description": "With pending."
                    },
                    "apiKey": {
                      "type": "string",
                      "description": "With approved."
                    },
                    "email": {
                      "type": "string"
                    },
                    "newAccount": {
                      "type": "boolean",
                      "description": "True when the account was created by this request."
                    },
                    "plan": {
                      "type": "string"
                    },
                    "creditsRemaining": {
                      "type": "number"
                    },
                    "appUrl": {
                      "type": "string"
                    },
                    "note": {
                      "type": "string",
                      "description": "Present when creditsRemaining is 0: how the person unlocks credits."
                    }
                  }
                },
                "examples": {
                  "pending": {
                    "value": {
                      "status": "pending",
                      "pollAfterSeconds": 5
                    }
                  },
                  "approved": {
                    "value": {
                      "status": "approved",
                      "apiKey": "YOUR_NEW_API_KEY",
                      "email": "maria@example.com",
                      "newAccount": true,
                      "plan": "free",
                      "creditsRemaining": 6,
                      "appUrl": "https://app.pedra.ai"
                    }
                  },
                  "denied": {
                    "value": {
                      "status": "denied"
                    }
                  },
                  "expired": {
                    "value": {
                      "status": "expired"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown requestId.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Unknown requestId"
                }
              }
            }
          },
          "405": {
            "description": "Wrong HTTP method.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Method not allowed"
                }
              }
            }
          }
        }
      }
    }
  },
  "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."
      },
      "SourceName": {
        "type": "string",
        "maxLength": 200,
        "description": "Optional name for the original photo (e.g. its file name). Requires `propertyId`. If the photo isn't in that property yet, it is saved there under this name in the same call (no need to call add_images_to_property first) and returned as `source.name`. A photo already in the property keeps its current name. Never drawn on the image."
      },
      "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",
              "sky-transition",
              "lite-zoom-in",
              "lite-zoom-out",
              "static"
            ],
            "description": "Frame animation. \"zoom-in\" (default) and \"zoom-out\" are AI zooms (5 credits). \"transition\" morphs into `secondImageUrl`; \"sky-transition\" keeps the scene still and changes only the sky and light into `secondImageUrl` (same shot, e.g. day to night); both 5 credits. \"lite-zoom-in\" / \"lite-zoom-out\" are a free, subtler 3D push (no AI video, 0 credits). \"static\" is a still frame (0 credits)."
          },
          "secondImageUrl": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ImageInput"
              }
            ],
            "description": "Target image for the \"transition\" and \"sky-transition\" effects. Required when `effect` is either of them."
          },
          "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 containing exactly one result image.",
        "properties": {
          "message": {
            "type": "string",
            "examples": [
              "Success"
            ]
          },
          "output": {
            "type": "array",
            "minItems": 1,
            "maxItems": 1,
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string",
                  "description": "URL of a generated image.",
                  "examples": [
                    "https://img.pedra.ai/generated-id"
                  ]
                }
              }
            },
            "description": "Always contains exactly one image: each endpoint processes a single `imageUrl`."
          },
          "source": {
            "type": "object",
            "description": "The original photo this result was made from. Present when `propertyId` was sent and the input photo is already in that property, or when `name` was sent too (the photo is then saved into the property).",
            "properties": {
              "imageId": {
                "type": "string"
              },
              "name": {
                "type": "string",
                "nullable": true
              }
            }
          }
        }
      },
      "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"
                ]
              }
            }
          },
          "source": {
            "type": "object",
            "description": "The original photo this result was made from. Present when `propertyId` was sent and the input photo is already in that property, or when `name` was sent too (the photo is then saved into the property).",
            "properties": {
              "imageId": {
                "type": "string"
              },
              "name": {
                "type": "string",
                "nullable": true
              }
            }
          }
        }
      },
      "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",
        "title": "Error",
        "description": "Returned with every non-2xx status. `error` is always present.",
        "required": [
          "error"
        ],
        "additionalProperties": false,
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error description.",
            "examples": [
              "Invalid imageUrl",
              "Insufficient credits",
              "User not found",
              "Method not allowed"
            ]
          }
        }
      },
      "TourScene": {
        "type": "object",
        "title": "TourScene",
        "description": "One 360° photo in a tour. Give exactly one of imageUrl or imageId. A plain string is treated as an imageUrl.",
        "properties": {
          "imageUrl": {
            "type": "string",
            "description": "Public URL or data: URI of an equirectangular 360° photo (2:1, 1.9–2.1 accepted, max 80 MB). Dropbox and Google Drive share links are converted to direct downloads."
          },
          "imageId": {
            "type": "string",
            "description": "A 360° photo already in the property (from /api/list_property_images with type \"360\")."
          },
          "name": {
            "type": "string",
            "description": "Room name. Omit it and AI names the room in the tour's language (free). Names you pass are never changed."
          }
        }
      },
      "TourSceneInput": {
        "title": "TourSceneInput",
        "oneOf": [
          {
            "$ref": "#/components/schemas/TourScene"
          },
          {
            "type": "string",
            "description": "Shorthand for { imageUrl }."
          }
        ]
      },
      "TourLink": {
        "type": "object",
        "title": "TourLink",
        "description": "A one-way navigation point shown in fromSceneId that leads to toSceneId.",
        "required": [
          "fromSceneId",
          "toSceneId",
          "yaw"
        ],
        "properties": {
          "linkId": {
            "type": "string",
            "description": "Returned only; ignored on input."
          },
          "fromSceneId": {
            "type": "string"
          },
          "toSceneId": {
            "type": "string"
          },
          "yaw": {
            "type": "number",
            "minimum": -180,
            "maximum": 180,
            "description": "Horizontal angle in the fromScene photo. 0 = centre of the equirectangular photo, negative = left."
          },
          "pitch": {
            "type": "number",
            "minimum": -90,
            "maximum": 90,
            "default": 0,
            "description": "Vertical angle. 0 = horizon."
          },
          "aiGenerated": {
            "type": "boolean",
            "description": "Returned only: true when the AI placed this link."
          }
        }
      },
      "TourLanguage": {
        "type": "string",
        "enum": [
          "en",
          "es",
          "fr",
          "de",
          "it",
          "pt"
        ],
        "default": "en",
        "description": "Tour page UI language and the language of AI room names. Non-English tours get a /{lang}/virtual-tour/... URL."
      },
      "VirtualTourSummary": {
        "type": "object",
        "title": "VirtualTourSummary",
        "properties": {
          "tourId": {
            "type": "string"
          },
          "propertyId": {
            "type": "string"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "processing",
              "ready",
              "failed"
            ]
          },
          "tourUrl": {
            "type": "string",
            "description": "Public tour page. Stable across edits."
          },
          "embedCode": {
            "type": "string",
            "description": "Ready-to-paste <iframe> for the tour."
          },
          "appUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "The property in the Pedra app."
          },
          "shareable": {
            "type": "boolean",
            "description": "false on free-plan accounts: the tour builds, but its public link shows an upgrade page."
          },
          "shareableNote": {
            "type": "string",
            "description": "Present when shareable is false."
          },
          "sceneCount": {
            "type": "integer"
          },
          "linkCount": {
            "type": "integer"
          },
          "coverImageUrl": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "progress": {
            "type": "object",
            "description": "Present while status is processing.",
            "properties": {
              "stage": {
                "type": "string",
                "enum": [
                  "queued",
                  "importing",
                  "naming",
                  "linking"
                ]
              },
              "done": {
                "type": "integer"
              },
              "total": {
                "type": "integer"
              }
            }
          },
          "error": {
            "type": "string",
            "description": "Present when status is failed."
          },
          "lastError": {
            "type": "string",
            "description": "Present on a ready tour whose last add_virtual_tour_scenes failed."
          },
          "failedScenes": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "index": {
                  "type": "integer"
                },
                "imageUrl": {
                  "type": "string"
                },
                "error": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "VirtualTour": {
        "title": "VirtualTour",
        "allOf": [
          {
            "$ref": "#/components/schemas/VirtualTourSummary"
          },
          {
            "type": "object",
            "properties": {
              "scenes": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "sceneId": {
                      "type": "string"
                    },
                    "name": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "imageUrl": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              },
              "links": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TourLink"
                }
              },
              "settings": {
                "type": "object",
                "properties": {
                  "navigationStyle": {
                    "type": "string",
                    "enum": [
                      "white",
                      "blue"
                    ]
                  },
                  "navigationSize": {
                    "type": "string",
                    "enum": [
                      "small",
                      "medium",
                      "large"
                    ]
                  },
                  "showLabels": {
                    "type": "boolean"
                  },
                  "language": {
                    "type": "string",
                    "enum": [
                      "en",
                      "es",
                      "fr",
                      "de",
                      "it",
                      "pt"
                    ]
                  }
                }
              }
            }
          }
        ]
      },
      "TourError": {
        "type": "object",
        "title": "TourError",
        "description": "Error body for the virtual tour endpoints.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "insufficient_credits",
              "tour_exists",
              "tour_processing",
              "tour_failed",
              "no_panoramas",
              "no_new_scenes"
            ]
          },
          "tourId": {
            "type": "string",
            "description": "With tour_exists: the property's existing tour."
          },
          "creditsRequired": {
            "type": "integer"
          },
          "creditsAvailable": {
            "type": "integer"
          },
          "upgradeUrl": {
            "type": "string"
          }
        }
      },
      "UploadLimitError": {
        "type": "object",
        "title": "UploadLimitError",
        "description": "A daily limit was reached (HTTP 429).",
        "required": [
          "error",
          "code"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "upload_limit",
              "upload_link_limit",
              "rate_limited"
            ],
            "description": "upload_limit: daily upload limit (30/day free, 500/day paid, reset at midnight UTC; the whole call is refused). upload_link_limit: upload links per day (5 free, 50 paid). rate_limited: agent signup requests."
          }
        }
      },
      "AgentSignupError": {
        "type": "object",
        "title": "AgentSignupError",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "disposable_email",
              "rate_limited",
              "unavailable"
            ]
          }
        }
      },
      "SocialNetwork": {
        "type": "string",
        "enum": [
          "instagram",
          "facebook",
          "tiktok",
          "youtube"
        ],
        "description": "A supported social network."
      },
      "SocialPostStatus": {
        "type": "string",
        "enum": [
          "pending",
          "processing",
          "posted",
          "failed",
          "cancelled"
        ],
        "description": "pending: waiting for its time (only pending posts can be cancelled). processing: being published. posted: live on the network. failed: not published, see error. cancelled: cancelled before it went out."
      },
      "SocialPost": {
        "type": "object",
        "title": "SocialPost",
        "required": [
          "postId",
          "network",
          "status",
          "scheduledAt",
          "postedAt",
          "platformPostId",
          "videoId",
          "error"
        ],
        "properties": {
          "postId": {
            "type": "string",
            "description": "Pedra's id for the post."
          },
          "network": {
            "$ref": "#/components/schemas/SocialNetwork"
          },
          "status": {
            "$ref": "#/components/schemas/SocialPostStatus"
          },
          "scheduledAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the post is due, in UTC."
          },
          "postedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When it went out, or null."
          },
          "platformPostId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The network's id once posted (e.g. the YouTube video id). For private TikTok posts, the publish id."
          },
          "videoId": {
            "type": "string",
            "description": "The stored video, shared by all posts from the same call."
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why the post failed, or null."
          },
          "firstCommentError": {
            "type": "string",
            "description": "Only present when a Facebook first comment failed. The post itself still went out."
          }
        }
      },
      "SocialPostInput": {
        "type": "object",
        "title": "SocialPostInput",
        "description": "One entry per network. Which extra fields apply depends on network: YouTube needs title, TikTok needs privacyLevel.",
        "required": [
          "network"
        ],
        "properties": {
          "network": {
            "$ref": "#/components/schemas/SocialNetwork"
          },
          "caption": {
            "type": "string",
            "maxLength": 5000,
            "description": "Post text; on YouTube it's the description. Max 2,200 characters on Instagram and TikTok, 5,000 on YouTube."
          },
          "title": {
            "type": "string",
            "maxLength": 100,
            "description": "YouTube only, required there. Title and caption can't contain < or >."
          },
          "privacyStatus": {
            "type": "string",
            "enum": [
              "public",
              "unlisted",
              "private"
            ],
            "default": "public",
            "description": "YouTube only."
          },
          "categoryId": {
            "type": "string",
            "description": "YouTube only. YouTube category number as a string, e.g. \"22\"."
          },
          "videoType": {
            "type": "string",
            "enum": [
              "REEL",
              "FEED"
            ],
            "default": "REEL",
            "description": "Facebook only."
          },
          "firstComment": {
            "type": "string",
            "maxLength": 8000,
            "description": "Facebook only. Posted as the Page's first comment, e.g. a link."
          },
          "privacyLevel": {
            "type": "string",
            "enum": [
              "PUBLIC_TO_EVERYONE",
              "MUTUAL_FOLLOW_FRIENDS",
              "FOLLOWER_OF_CREATOR",
              "SELF_ONLY"
            ],
            "description": "TikTok only, required there. Must be one the creator's account allows."
          },
          "disableComment": {
            "type": "boolean",
            "description": "TikTok only."
          },
          "disableDuet": {
            "type": "boolean",
            "description": "TikTok only."
          },
          "disableStitch": {
            "type": "boolean",
            "description": "TikTok only."
          },
          "brandOrganicToggle": {
            "type": "boolean",
            "description": "TikTok only. The video promotes your own brand."
          },
          "brandContentToggle": {
            "type": "boolean",
            "description": "TikTok only. Paid partnership; not allowed with SELF_ONLY."
          },
          "aiGenerated": {
            "type": "boolean",
            "default": true,
            "description": "TikTok only. TikTok's AI-generated content label; set false only for real footage."
          }
        }
      },
      "SocialError": {
        "type": "object",
        "title": "SocialError",
        "description": "Error body for the social endpoints.",
        "required": [
          "error",
          "code"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "invalid_video_url",
              "video_unreachable",
              "not_a_video",
              "video_too_large",
              "invalid_network",
              "not_connected",
              "invalid_post",
              "invalid_scheduled_at",
              "invalid_cover",
              "invalid_api_key",
              "pro_required",
              "daily_limit",
              "post_not_found",
              "not_cancellable"
            ]
          }
        }
      }
    }
  }
}
